├── lib └── net │ ├── yail │ ├── yail-version.rb │ ├── IRCBot.rb │ ├── handler.rb │ ├── dispatch.rb │ ├── default_events.rb │ ├── magic_events.rb │ ├── message_parser.rb │ ├── output_api.rb │ ├── report_events.rb │ ├── irc_bot.rb │ ├── legacy_events.rb │ ├── eventmap.yml │ └── event.rb │ └── yail.rb ├── examples ├── logger │ ├── default.yml │ ├── run.rb │ └── logger_bot.rb └── simple │ ├── dumbbot.rb │ ├── kick.rb │ └── whois.rb ├── tests ├── net_yail.rb ├── tc_message_parser.rb ├── mock_irc.rb ├── tc_event.rb └── tc_yail.rb ├── Rakefile ├── README.md └── CHANGELOG /lib/net/yail/yail-version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class YAIL 3 | VERSION = '1.6.5' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /examples/logger/default.yml: -------------------------------------------------------------------------------- 1 | silent: false 2 | loud: false 3 | output-dir: /tmp/logs 4 | master: Nerdmaster 5 | passwords: 6 | "#protected": "my_secret_password" 7 | -------------------------------------------------------------------------------- /tests/net_yail.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib") 2 | require 'net/yail' 3 | require 'net/yail/message_parser' 4 | require 'net/yail/event' 5 | puts "VERSION: #{Net::YAIL::VERSION}" 6 | -------------------------------------------------------------------------------- /lib/net/yail/IRCBot.rb: -------------------------------------------------------------------------------- 1 | # This is a deprecated file! 2 | warn '[DEPRECATED] Requiring "net/yail/IRCBot" is deprecated! Use "net/yail/irc_bot" instead.' 3 | 4 | # Wrapper for irc_bot for backward-compatibility 5 | require 'rubygems' 6 | require 'net/yail' 7 | require 'net/yail/irc_bot' 8 | -------------------------------------------------------------------------------- /examples/simple/dumbbot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Want a specific version of net/yail? Try uncommenting this: 4 | # gem 'net-yail', '1.x.y' 5 | 6 | require 'net/yail' 7 | require 'getopt/long' 8 | 9 | # User specifies channel and nick 10 | opt = Getopt::Long.getopts( 11 | ['--network', Getopt::REQUIRED], 12 | ['--nick', Getopt::REQUIRED], 13 | ['--port', Getopt::REQUIRED], 14 | ['--loud', Getopt::BOOLEAN] 15 | ) 16 | 17 | opts = { 18 | :address => opt['network'], 19 | :username => 'FrakkingBot', 20 | :realname => 'John Botfrakker', 21 | :nicknames => [opt['nick']], 22 | } 23 | opts[:port] = opt['port'] if opt['port'] 24 | 25 | irc = Net::YAIL.new(opts) 26 | 27 | irc.log.level = Logger::DEBUG if opt['loud'] 28 | 29 | # Register handlers 30 | irc.heard_welcome { |e| irc.join('#bots') } # Filter - runs after the server's welcome message is read 31 | irc.on_invite { |e| irc.join(e.channel) } # Handler - runs on an invite message 32 | 33 | # Start the bot and enjoy the endless loop 34 | irc.start_listening! 35 | -------------------------------------------------------------------------------- /examples/simple/kick.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Want a specific version of net/yail? Try uncommenting this: 4 | # gem 'net-yail', '1.x.y' 5 | 6 | require 'net/yail' 7 | require 'getopt/long' 8 | 9 | # User specifies channel and nick 10 | opt = Getopt::Long.getopts( 11 | ['--network', Getopt::REQUIRED], 12 | ['--nick', Getopt::REQUIRED], 13 | ['--port', Getopt::REQUIRED], 14 | ['--loud', Getopt::BOOLEAN] 15 | ) 16 | 17 | opts = { 18 | :address => opt['network'], 19 | :username => 'FrakkingBot', 20 | :realname => 'John Botfrakker', 21 | :nicknames => [opt['nick']], 22 | } 23 | opts[:port] = opt['port'] if opt['port'] 24 | 25 | irc = Net::YAIL.new(opts) 26 | 27 | irc.log.level = Logger::DEBUG if opt['loud'] 28 | 29 | # Register handlers 30 | irc.heard_welcome { |e| irc.join('#bots') } # Filter - runs after the server's welcome message is read 31 | 32 | # KICK example - kicks everybody from the channel other than self 33 | data = {} 34 | irc.heard_join do |e| 35 | irc.kick(e.nick, e.channel, "I'm USING THE BATHROOM! Give me a minute!") unless e.nick == irc.me 36 | end 37 | 38 | # Start the bot and enjoy the endless loop 39 | irc.start_listening! 40 | -------------------------------------------------------------------------------- /lib/net/yail/handler.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class YAIL 3 | 4 | # Represents a method and meta-data for handling an event 5 | class Handler 6 | def initialize(method, conditions = {}) 7 | @method = method 8 | 9 | # Make sure even an explicit nil is turned into an empty hash 10 | @conditions = conditions || {} 11 | end 12 | 13 | # Calls the handler with the given arguments if the conditions are met 14 | def call(event) 15 | # Get out if :if/:unless aren't met 16 | return if @conditions[:if] && !condition_check(@conditions[:if], event) 17 | return if @conditions[:unless] && condition_check(@conditions[:unless], event) 18 | 19 | return @method.call(event) 20 | end 21 | 22 | # Checks the condition. Procs are simply run and returned, while Hash-based conditions return 23 | # true if value === event.send(key) 24 | def condition_check(condition, event) 25 | # Procs are the easiest to evaluate 26 | return condition.call(event) if condition.is_a?(Proc) 27 | 28 | # If not a proc, condition must be a hash - iterate over values. All must be true to 29 | # return true. 30 | for (key, value) in condition 31 | return false unless value === event.send(key) 32 | end 33 | return true 34 | end 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /examples/logger/run.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'logger_bot' 5 | require 'getopt/long' 6 | 7 | # This code just launches our logger with certain parameters. 8 | # 9 | # If options are specified, they override the default yaml file. If a 10 | # different yaml file is specified, its settings are read, then overridden 11 | # by options. 12 | # 13 | # Passwords hash cannot be specified on the command-line - yaml or nothing. 14 | 15 | opt = Getopt::Long.getopts( 16 | ['--silent', '-s', Getopt::BOOLEAN], 17 | ['--loud', '-l', Getopt::BOOLEAN], 18 | ['--network', '-n', Getopt::OPTIONAL], 19 | ['--output-dir', '-o', Getopt::OPTIONAL], 20 | ['--master', '-m', Getopt::OPTIONAL], 21 | ['--yaml', '-y', Getopt::OPTIONAL] 22 | ) 23 | 24 | opt['yaml'] ||= File.dirname(__FILE__) + '/default.yml' 25 | if File.exists?(opt['yaml']) 26 | options = File.open(opt['yaml']) {|f| YAML::load(f)} 27 | else 28 | options = {} 29 | end 30 | 31 | for key in %w{silent loud network output-dir master} 32 | options[key] ||= opt[key] 33 | end 34 | 35 | @bot = LoggerBot.new( 36 | :silent => options['silent'], 37 | :loud => options['loud'], 38 | :irc_network => options['network'], 39 | :output_dir => options['output-dir'], 40 | :master => options['master'], 41 | :passwords => options['passwords'] 42 | ) 43 | @bot.irc_loop 44 | -------------------------------------------------------------------------------- /examples/simple/whois.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Want a specific version of net/yail? Try uncommenting this: 4 | # gem 'net-yail', '1.x.y' 5 | 6 | require 'net/yail' 7 | require 'getopt/long' 8 | 9 | # User specifies channel and nick 10 | opt = Getopt::Long.getopts( 11 | ['--network', Getopt::REQUIRED], 12 | ['--nick', Getopt::REQUIRED], 13 | ['--port', Getopt::REQUIRED], 14 | ['--loud', Getopt::BOOLEAN] 15 | ) 16 | 17 | opts = { 18 | :address => opt['network'], 19 | :username => 'FrakkingBot', 20 | :realname => 'John Botfrakker', 21 | :nicknames => [opt['nick']], 22 | } 23 | opts[:port] = opt['port'] if opt['port'] 24 | 25 | irc = Net::YAIL.new(opts) 26 | 27 | irc.log.level = Logger::DEBUG if opt['loud'] 28 | 29 | # Register handlers 30 | irc.heard_welcome { |e| irc.join('#bots') } # Filter - runs after the server's welcome message is read 31 | irc.on_invite { |e| irc.join(e.channel) } # Handler - runs on an invite message 32 | 33 | # WHOIS example (this could be useful for other numerics as well) 34 | data = {} 35 | irc.heard_join do |e| 36 | data = {:nick => e.nick} 37 | irc.whois(e.nick) 38 | end 39 | irc.heard_whoisuser do |e| 40 | data[:name] = e.parameters[4] 41 | data[:host] = e.parameters[2] 42 | end 43 | irc.heard_whoischannels do |e| 44 | data[:channels] = e.parameters.last 45 | end 46 | irc.heard_endofwhois do |e| 47 | irc.msg(data[:nick], "I know who you are, #{data[:nick]}") 48 | irc.msg(data[:nick], data.inspect) 49 | end 50 | 51 | # Start the bot and enjoy the endless loop 52 | irc.start_listening! 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems/package_task' 2 | require 'rake/testtask' 3 | require './lib/net/yail/yail-version' 4 | spec = Gem::Specification.new do |s| 5 | s.platform = Gem::Platform::RUBY 6 | s.name = "net-yail" 7 | s.version = Net::YAIL::VERSION 8 | s.author = "Jeremy Echols" 9 | s.email = "yailnerdbucket dot com" 10 | s.description = %Q| 11 | Net::YAIL is an IRC library written in pure Ruby. Using simple functions, it 12 | is trivial to build a complex, event-driven IRC application, such as a bot or 13 | even a full command-line client. All events can have a single callback and 14 | any number of before-callback and after-callback filters. Even outgoing events, 15 | such as when you join a channel or send a message, can have filters for stats 16 | gathering, text filtering, etc. 17 | |.strip 18 | 19 | s.summary = "Yet Another IRC Library: wrapper for IRC communications in Ruby." 20 | s.files = FileList[ 'examples/simple/*', 'examples/logger/*', 'examples/loudbot/*.rb', 'lib/net/*.rb', 'lib/net/yail/*', 'test/*.rb' ].to_a 21 | s.homepage = 'http://ruby-irc-yail.nerdbucket.com/' 22 | s.rubyforge_project = 'net-yail' 23 | s.require_path = "lib" 24 | s.test_files = Dir.glob('tests/*.rb') 25 | s.has_rdoc = true 26 | s.rdoc_options = ['-m', 'Net::YAIL', '-f', 'sdoc'] 27 | end 28 | 29 | Gem::PackageTask.new(spec) do |pkg| 30 | pkg.need_tar = true 31 | end 32 | 33 | Rake::TestTask.new do |t| 34 | t.libs << "tests" 35 | t.test_files = FileList['tests/tc_*.rb'] 36 | t.verbose = true 37 | end 38 | 39 | task :default => "pkg/#{spec.name}-#{spec.version}.gem" do 40 | puts "generated latest version" 41 | end 42 | -------------------------------------------------------------------------------- /lib/net/yail/dispatch.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class YAIL 3 | 4 | module Dispatch 5 | # Given an event, calls pre-callback filters, callback, and post-callback filters. Uses 6 | # *_any event, where * is the event's event_class value 7 | def dispatch(event) 8 | # We always have an "any" filter option, so we build the symbol first 9 | any_filter_sym = (event.event_class + "_any").to_sym 10 | 11 | before_any = @before_filters[any_filter_sym] 12 | run_chain(event, :allow_halt => true, :handlers => [before_any, @before_filters[event.type]]) 13 | 14 | # Have to break here if before filters said so 15 | return if event.handled? 16 | 17 | # Legacy handler - return if true, since that's how the old system works - EXCEPTION for outgoing events, since 18 | # the old system didn't allow the outgoing "core" code to be skipped! 19 | if true == legacy_process_event(event) 20 | return unless Net::YAIL::OutgoingEvent === event 21 | end 22 | 23 | # Add new callback and all after-callback stuff to a new chain 24 | after_any = @after_filters[any_filter_sym] 25 | run_chain(event, :allow_halt => false, :handlers => [@callback[event.type], @after_filters[event.type], after_any]) 26 | end 27 | 28 | # Consolidates all handlers passed in, flattening into a single array of handlers and 29 | # removing any nils, then runs the methods on each event, respecting filter conditions 30 | def run_chain(event, opts = {}) 31 | handlers = opts[:handlers] 32 | handlers.flatten! 33 | handlers.compact! 34 | 35 | allow_halt = opts[:allow_halt] 36 | 37 | # Run each filter in the chain if conditions are met, exiting early if event was handled 38 | for handler in handlers 39 | handler.call(event) 40 | return if event.handled? && true == allow_halt 41 | end 42 | 43 | end 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/net/yail/default_events.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module IRCEvents 3 | 4 | # This module contains all the default events handling that hasn't yet been cleaned up for 2.0 5 | module Defaults 6 | private 7 | 8 | # Nickname change failed: already in use. This needs a rewrite to at 9 | # least hit a "failed too many times" handler of some kind - for a bot, 10 | # quitting may be fine, but for something else, we may want to prompt a 11 | # user or try again in 20 minutes or something. Note that we only fail 12 | # when the adapter hasn't gotten logged in yet - an attempt at changing 13 | # nick after registration (welcome message) just generates a log message. 14 | # 15 | # TODO: This should really not even be here. Client should have full control over whether or not 16 | # they want this. Base IRC bot class should have this, but not the core YAIL lib. 17 | def _nicknameinuse(event) 18 | event.message =~ /^(\S+)/ 19 | @log.warn "Nickname #{$1} is already in use." 20 | 21 | if (!@registered) 22 | begin 23 | nextnick = @nicknames[(0...@nicknames.length).find { |i| @nicknames[i] == $1 } + 1] 24 | if (nextnick != nil) 25 | nick nextnick 26 | else 27 | @log.error '*** All nicknames in use. ***' 28 | raise ArgumentError.new("All nicknames in use") 29 | end 30 | rescue 31 | @log.error '*** Nickname selection error. ***' 32 | raise 33 | end 34 | end 35 | end 36 | 37 | # Names line 38 | # 39 | # TODO: Either store this data silently or ditch this code - this verbosity doesn't belong in a core lib 40 | def _namreply(event) 41 | event.message =~ /^(@|\*|=) (\S+) :?(.+)$/ 42 | channeltype = {'@' => 'Secret', '*' => 'Private', '=' => 'Normal'}[$1] 43 | @log.info "{#{$2}} #{channeltype} channel nickname list: #{$3}" 44 | @nicklist = $3.split(' ') 45 | @nicklist.collect!{|name| name.sub(/^\W*/, '')} 46 | @log.info "First nick: #{@nicklist[0]}" 47 | end 48 | 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/net/yail/magic_events.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module IRCEvents 3 | 4 | # This module contains all the "magic" methods that need to happen by default. User could overwrite 5 | # some of these, but really really shouldn't. 6 | module Magic 7 | private 8 | 9 | # We dun connected to a server! Just sends password (if one is set) and 10 | # user/nick. This isn't quite "essential" to a working IRC app, but this data 11 | # *must* be sent at some point, so be careful before clobbering this handler. 12 | def out_begin_connection(event) 13 | pass(@password) if @password 14 | user(event.username, '0.0.0.0', event.address, event.realname) 15 | nick(@nicknames[0]) 16 | end 17 | 18 | # We were welcomed, so we need to set up initial nickname and set that we 19 | # registered so nick change failure doesn't cause DEATH! 20 | def magic_welcome(event) 21 | @log.info "#{event.from} welcome message: #{event.message}" 22 | if (event.message =~ /(\S+)!\S+$/) 23 | @me = $1 24 | elsif (event.message =~ /(\S+)$/) 25 | @me = $1 26 | end 27 | 28 | @registered = true 29 | mode @me, 'i' 30 | end 31 | 32 | # Ping must have a PONG, though crazy user can handle this her own way if she likes 33 | def magic_ping(event); @socket.puts "PONG :#{event.message}"; end 34 | 35 | # If bot changes his name, @me must change - this must be a filter, not the callback! 36 | def magic_nick(event) 37 | @me = event.message.dup if event.nick.downcase == @me.downcase 38 | end 39 | 40 | # User calls msg, sends a simple message out to the event's target (user or channel) 41 | def magic_out_msg(event) 42 | privmsg(event.target, event.message) 43 | end 44 | 45 | # CTCP 46 | def magic_out_ctcp(event) 47 | privmsg(event.target, "\001#{event.message}\001") 48 | end 49 | 50 | # CTCP ACTION 51 | def magic_out_act(event) 52 | privmsg(event.target, "\001ACTION #{event.message}\001") 53 | end 54 | 55 | # WHOIS - here because first parameter might be the nick and might be the optional server 56 | def magic_out_whois(event) 57 | string = "WHOIS %s%s" % [event.server.to_s.empty? ? "" : event.server, event.nick] 58 | raw string 59 | end 60 | 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/net/yail/message_parser.rb: -------------------------------------------------------------------------------- 1 | # Net::YAIL's solution to the amazing lack of *useful* IRC message parsers. So far as I know, 2 | # this will parse any message coming from an RFC-compliant IRC server. 3 | 4 | module Net 5 | class YAIL 6 | 7 | # This is my lame attempt to convert the BNF-style grammar from RFC 1459 into 8 | # useable ruby regexes. The hope here is that one can effectively match an 9 | # incoming message with high accuracy. Usage: 10 | # 11 | # line = ':Nerdmaster!jeremy@nerdbucket.com PRIVMSG Nerdminion :Do my bidding!!' 12 | # message = Net::YAIL::MessageParser.new(line) 13 | # # hash now has all kinds of useful pieces of the incoming message: 14 | # puts line.nick # "Nerdmaster" 15 | # puts line.user # "jeremy" 16 | # puts line.host # "nerdbucket.com" 17 | # puts line.prefix # "Nerdmaster!jeremy@nerdbucket.com" 18 | # puts line.command # "PRIVMSG" 19 | # puts line.params # ["Nerdminion", "Do my bidding!!"] 20 | class MessageParser 21 | attr_reader :nick, :user, :host, :prefix, :command, :params, :servername 22 | 23 | # Note that all regexes are non-greedy. I'm scared of greedy regexes, sirs. 24 | USER = /\S+?/ 25 | # RFC suggested that a nick *had* to start with a letter, but that seems to 26 | # not be the case. Oh, and also, pretty much anything is allowed to be a 27 | # nick it turns out, at least in practice. 28 | NICK = /[^! ]+?/ 29 | HOST = /\S+?/ 30 | SERVERNAME = /\S+?/ 31 | 32 | # This is automatically grouped for ease of use in the parsing. Group 1 is 33 | # the full prefix; 2, 3, and 4 are nick/user/host; 1 is also servername if 34 | # there was no match to populate 2, 3, and 4. 35 | PREFIX = /((#{NICK})!(#{USER})@(#{HOST})|#{SERVERNAME})/ 36 | COMMAND = /(\w+|\d{3})/ 37 | TRAILING = /\:\S*?/ 38 | MIDDLE = /(?: +([^ :]\S*))/ 39 | 40 | MESSAGE = /^(?::#{PREFIX} +)?#{COMMAND}(.*)$/ 41 | 42 | def initialize(line) 43 | @params = [] 44 | 45 | if line =~ MESSAGE 46 | matches = Regexp.last_match 47 | 48 | @prefix = matches[1] 49 | if (matches[2]) 50 | @nick = matches[2] 51 | @user = matches[3] 52 | @host = matches[4] 53 | else 54 | @servername = matches[1] 55 | end 56 | 57 | @command = matches[5] 58 | 59 | # Args are a bit tricky. First off, we know there must be a single 60 | # space before the arglist, so we need to strip that. Then we have to 61 | # separate the trailing arg as it can contain nearly any character. And 62 | # finally, we split the "middle" args on space. 63 | arglist = matches[6].sub(/^ +/, '') 64 | arglist.sub!(/^:/, ' :') 65 | (middle_args, trailing_arg) = arglist.split(/ +:/, 2) 66 | @params.push(middle_args.split(/ +/), trailing_arg) 67 | @params.compact! 68 | @params.flatten! 69 | end 70 | end 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /tests/tc_message_parser.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/net_yail' 3 | require 'test/unit' 4 | 5 | class MessageParserTest < Test::Unit::TestCase 6 | # Very simple parsing of easy strings 7 | def test_parse_basic 8 | # Basic test of privmsg-type command 9 | msg = Net::YAIL::MessageParser.new(':Nerdmaster!jeremy@nerdbucket.com PRIVMSG Nerdminion :Do my bidding!!') 10 | assert_equal 'Nerdmaster', msg.nick 11 | assert_equal 'jeremy', msg.user 12 | assert_equal 'nerdbucket.com', msg.host 13 | assert_equal 'Nerdmaster!jeremy@nerdbucket.com', msg.prefix 14 | assert_equal 'PRIVMSG', msg.command 15 | assert_equal 'Nerdminion', msg.params[0] 16 | assert_equal 'Do my bidding!!', msg.params[1] 17 | 18 | # Server command of some type 19 | msg = Net::YAIL::MessageParser.new(':nerdbucket.com SERVERCOMMAND arg1 arg2 :final :trailing :arg, --fd9823') 20 | assert_equal 'nerdbucket.com', msg.servername 21 | assert_nil msg.user 22 | assert_nil msg.nick 23 | assert_nil msg.host 24 | assert_equal 'nerdbucket.com', msg.prefix 25 | assert_equal 'arg1', msg.params[0] 26 | assert_equal 'arg2', msg.params[1] 27 | assert_equal 'final :trailing :arg, --fd9823', msg.params[2] 28 | 29 | # Server command of some type - no actual final arg 30 | msg = Net::YAIL::MessageParser.new(':nerdbucket.com SERVERCOMMAND arg1:finaltrailingarg') 31 | assert_equal 'nerdbucket.com', msg.servername 32 | assert_nil msg.user 33 | assert_nil msg.nick 34 | assert_nil msg.host 35 | assert_equal 'nerdbucket.com', msg.prefix 36 | assert_equal 'arg1:finaltrailingarg', msg.params[0] 37 | 38 | # WTF? Well, IRC spec says it's valid 39 | msg = Net::YAIL::MessageParser.new('MAGICFUNKYFRESHCMD arg1 arg2') 40 | assert_nil msg.servername 41 | assert_equal 'MAGICFUNKYFRESHCMD', msg.command 42 | assert_equal 'arg1', msg.params[0] 43 | assert_equal 'arg2', msg.params[1] 44 | 45 | # Action 46 | msg = Net::YAIL::MessageParser.new(":Nerdmaster!jeremy@nerdbucket.com PRIVMSG #bottest :\001ACTION gives Towelie a joint\001") 47 | assert_equal 'Nerdmaster', msg.nick 48 | assert_equal 'PRIVMSG', msg.command 49 | assert_equal '#bottest', msg.params.first 50 | assert_equal "\001ACTION gives Towelie a joint\001", msg.params.last 51 | 52 | # Bot sets mode 53 | msg = Net::YAIL::MessageParser.new(':Towelie!~x2e521146@towelie.foo.bar MODE Towelie :+i') 54 | assert_equal 'Towelie', msg.nick 55 | assert_equal 'towelie.foo.bar', msg.host 56 | assert_equal 'MODE', msg.command 57 | assert_equal 'Towelie', msg.params.first 58 | assert_equal '+i', msg.params.last 59 | 60 | # Numeric message with a : before final param 61 | msg = Net::YAIL::MessageParser.new(':someserver.co.uk.fn.bb 366 Towelie #bottest :End of /NAMES list.') 62 | assert_nil msg.nick 63 | assert_nil msg.host 64 | assert_equal '366', msg.command 65 | assert_equal 'Towelie', msg.params.shift 66 | assert_equal '#bottest', msg.params.shift 67 | assert_equal 'End of /NAMES list.', msg.params.shift 68 | 69 | # Nick change when nick is "unusual" - this also tests the bug with a single parameter being 70 | # treated incorrectly 71 | msg = Net::YAIL::MessageParser.new(':[|\|1]!~nerdmaste@nerd.nerdbucket.com NICK :Deadnerd') 72 | assert_equal '[|\|1]', msg.nick 73 | assert_equal 'NICK', msg.command 74 | assert_equal '~nerdmaste', msg.user 75 | assert_equal 'nerd.nerdbucket.com', msg.host 76 | assert_equal '[|\|1]!~nerdmaste@nerd.nerdbucket.com', msg.prefix 77 | assert_equal 'Deadnerd', msg.params.shift 78 | assert_equal 0, msg.params.length 79 | 80 | # Annoying topic change 81 | msg = Net::YAIL::MessageParser.new(":Dude!dude@nerdbucket.com TOPIC #nerdtalk :31 August 2010 \357\277\275 Foo.") 82 | assert_equal 'TOPIC', msg.command 83 | assert_equal 'Dude', msg.nick 84 | assert_equal "31 August 2010 \357\277\275 Foo.", msg.params.last 85 | assert_equal '#nerdtalk', msg.params.first 86 | assert_equal 'Dude!dude@nerdbucket.com', msg.prefix 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/net/yail/output_api.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | 3 | # This module is responsible for the raw socket output, buffering of all "message" types of 4 | # events, and exposing the magic to create a new output command + handler. All output methods 5 | # are documented in the main Net::YAIL documentation. 6 | module IRCOutputAPI 7 | # Spits a raw string out to the server - in case a subclass wants to do 8 | # something special on *all* output, please make all output go through this 9 | # method. Don't use puts manually. I will kill violaters. Legally 10 | # speaking, that is. 11 | def raw(line) 12 | @socket.puts "#{line}\r\n" 13 | end 14 | 15 | # Buffers the given event to be sent out when we are able to send something out to the given 16 | # target. If buffering isn't turned on, the event will be processed in the next loop of outgoing 17 | # messages. 18 | def buffer_output(event) 19 | @privmsg_buffer_mutex.synchronize do 20 | @privmsg_buffer[event.target] ||= Array.new 21 | @privmsg_buffer[event.target].push event 22 | end 23 | end 24 | 25 | # Buffers an :outgoing_msg event. Could be used to send any privmsg, but you're betting off 26 | # using act and ctcp shortcut methods for those types. Target is a channel or username, message 27 | # is the message. 28 | def msg(target, message) 29 | buffer_output Net::YAIL::OutgoingEvent.new(:type => :msg, :target => target, :message => message) 30 | end 31 | 32 | # Buffers an :outgoing_ctcp event. Target is user or channel, message is message. 33 | def ctcp(target, message) 34 | buffer_output Net::YAIL::OutgoingEvent.new(:type => :ctcp, :target => target, :message => message) 35 | end 36 | 37 | # Buffers an :outgoing_act event. Target is user or channel, message is message. 38 | def act(target, message) 39 | buffer_output Net::YAIL::OutgoingEvent.new(:type => :act, :target => target, :message => message) 40 | end 41 | 42 | # WHOIS is tricky - it can optionally take a :target parameter, which is the *first* parameter 43 | # if it's present, rather than added to the parameter list. BAD SPECIFICATIONS! NO BISCUIT! 44 | # (If it were more standard, we could just use create_command) 45 | # 46 | # Note that "nick" can actually be a comma-separated list of masks for whois querying. 47 | def whois(nick, server = nil) 48 | dispatch Net::YAIL::OutgoingEvent.new(:type => :whois, :nick => nick, :server => server) 49 | end 50 | 51 | # Creates an output command and its handler. output_base is a template of the command without 52 | # any conditional arguments (for simple commands this is the full template). args is a list of 53 | # argument symbols to determine how the event is built and handled. If an argument symbol is 54 | # followed by a string, that string is conditionally appended to the output in the handler if the 55 | # event has data for that argument. 56 | # 57 | # I hate the hackiness here, but it's so much easier to build the commands and handlers with an 58 | # ugly one-liner than manually, and things like define_method seem to fall short with how much 59 | # crap this needs to do. 60 | def create_command(command, output_base, *opts) 61 | event_opts = lambda {|text| text.gsub(/:(\w+)/, '#{event.\1}') } 62 | 63 | output_base = event_opts.call(output_base) 64 | 65 | # Create a list of actual arg symbols and templates for optional args 66 | args = [] 67 | optional_arg_templates = {} 68 | last_symbol = nil 69 | for opt in opts 70 | case opt 71 | when Symbol 72 | args.push opt 73 | last_symbol = opt 74 | when String 75 | raise ArgumentError.new("create_command optional argument must have an argument symbol preceding them") unless last_symbol 76 | optional_arg_templates[last_symbol] = event_opts.call(opt) 77 | last_symbol = nil 78 | end 79 | end 80 | 81 | # Format strings for command args and event creation 82 | event_string = args.collect {|arg| ":#{arg} => #{arg}"}.join(",") 83 | event_string = ", #{event_string}" unless event_string.empty? 84 | args_string = args.collect {|arg| "#{arg} = ''"}.join(",") 85 | 86 | # Create the command function 87 | command_code = %Q| 88 | def #{command}(#{args_string}) 89 | dispatch Net::YAIL::OutgoingEvent.new(:type => #{command.inspect}#{event_string}) 90 | end 91 | | 92 | self.class.class_eval command_code 93 | 94 | # Create the handler piece by piece - wow how ugly this is! 95 | command_handler = :"magic_out_#{command}" 96 | handler_code = %Q| 97 | def #{command_handler}(event) 98 | output_string = "#{output_base}" 99 | | 100 | for arg in args 101 | if optional_arg_templates[arg] 102 | handler_code += %Q| 103 | output_string += "#{optional_arg_templates[arg]}" unless event.#{arg}.to_s.empty? 104 | | 105 | end 106 | end 107 | handler_code += %Q| 108 | raw output_string 109 | end 110 | | 111 | 112 | self.class.class_eval handler_code 113 | 114 | # At least setting the callback isn't a giant pile of dumb 115 | set_callback :"outgoing_#{command}", self.method(command_handler) 116 | end 117 | end 118 | 119 | end 120 | -------------------------------------------------------------------------------- /examples/logger/logger_bot.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'rubygems' 4 | require 'net/yail/irc_bot' 5 | require 'date' 6 | 7 | class LoggerBot < IRCBot 8 | BOTNAME = 'Logger' 9 | BOTVERSION = 'v0.1.0' 10 | 11 | public 12 | # Starts a new instance 13 | # 14 | # Options: 15 | # * :irc_network: IP/name of server 16 | # * :port: ... 17 | # * :loud: Overly-verbose logging 18 | # * :silent: Very little logging 19 | # * :master: User who can order quits 20 | # * :output_dir: Where to store log files 21 | # * :passwords: Hash of channel=>pass for joining channels 22 | def initialize(options = {}) 23 | @master = options.delete(:master) 24 | @output_dir = options.delete(:output_dir) || File.dirname(__FILE__) 25 | 26 | # Log files per channel - logs rotate every so often, so we have to store 27 | # filenames on a per-channel basis 28 | @current_log = {} 29 | @log_date = {} 30 | @passwords = options[:passwords] || {} 31 | 32 | options[:username] = BOTNAME 33 | options[:realname] = BOTNAME 34 | options[:nicknames] = ['LoggerBot', 'Logger_Bot', 'logger_bot', '_LoggerBot', 'LoggerBot_'] 35 | 36 | # Set up IRCBot, our loving parent, and begin 37 | super(options) 38 | self.connect_socket 39 | self.start_listening 40 | end 41 | 42 | # Add hooks on startup (base class's start method calls add_custom_handlers) 43 | def add_custom_handlers 44 | # Set up hooks 45 | @irc.on_msg self.method(:_in_msg) 46 | @irc.on_act self.method(:_in_act) 47 | @irc.on_invite self.method(:_in_invited) 48 | @irc.on_kick self.method(:_in_kick) 49 | @irc.saying_join self.method(:_out_join) 50 | end 51 | 52 | private 53 | # Incoming message handler 54 | def _in_msg(event) 55 | # check if this is a /msg command, or normal channel talk 56 | if event.pm? 57 | incoming_private_message(event.nick, event.message) 58 | else 59 | incoming_channel_message(event.nick, event.channel, event.message) 60 | end 61 | end 62 | 63 | def _in_act(event) 64 | # check if this is a /msg command, or normal channel talk 65 | return if event.pm? 66 | log_channel_message(event.nick, event.channel, "#{event.nick} #{event.message}") 67 | end 68 | 69 | # TODO: recalls the most recent logs for a given channel by reading from 70 | # the file system or using a hash of log data. Or both. 71 | def recent_logs(channel, num = 10) 72 | raise "Not implemented" 73 | end 74 | 75 | # TODO: Searches logs for a given pattern. Relies on the file system data 76 | # to do a comprehensive search, and 'grep', so SANITIZE INPUT! 77 | def search_logs(pattern) 78 | raise "Not implemented" 79 | end 80 | 81 | # Gives the user very simplistic information. TODO: Add a command for 82 | # accessing and searching logs. 83 | def incoming_private_message(user, text) 84 | case text 85 | when /\bhelp\b/i 86 | msg(user, 'LoggerBot at your service - I log all messages and actions in any channel') 87 | msg(user, 'I\'m in. In the future I\'ll offer searchable logs. If you /INVITE me to') 88 | msg(user, 'a channel, I\'ll pop in and start logging.') 89 | return 90 | end 91 | 92 | msg(user, "I don't log private messages. If you'd like to know what I do, ") 93 | msg(user, "enter \"HELP\"") 94 | end 95 | 96 | def incoming_channel_message(user, channel, text) 97 | # check for special stuff before keywords 98 | # Nerdmaster is allowed to do special ordering 99 | if @master == user 100 | if (text == "#{bot_name}: QUIT") 101 | self.irc.quit("Ordered by my master") 102 | sleep 1 103 | exit 104 | end 105 | end 106 | 107 | case text 108 | when /^\s*#{bot_name}(:|)\s*uptime\s*$/i 109 | msg(channel, get_uptime_string) 110 | 111 | when /botcheck/i 112 | msg(channel, "#{BOTNAME} #{BOTVERSION}") 113 | 114 | else 115 | log_channel_message(user, channel, "<#{user}> #{text}") 116 | end 117 | end 118 | 119 | # Logs the message data to a flat text file. Fun. 120 | def log_channel_message(user, channel, text) 121 | today = Date.today 122 | if @current_log[channel].nil? || @log_date[channel] != today 123 | chan_dir = @output_dir + '/' + channel 124 | Dir::mkdir(chan_dir) unless File.exists?(chan_dir) 125 | filename = chan_dir + '/' + today.strftime('%Y%m%d') + '.log' 126 | @current_log[channel] = filename 127 | @log_date[channel] = today 128 | end 129 | 130 | time = Time.now.strftime '%H:%M:%S' 131 | File.open(@current_log[channel], 'a') do |f| 132 | f.puts "[#{time}] #{text}" 133 | end 134 | end 135 | 136 | # Invited to a channel for logging purposes - simply auto-join for now. 137 | # Maybe allow only @master one day, or array of authorized users. 138 | def _in_invited(event) 139 | join event.channel 140 | end 141 | 142 | # If bot is kicked, he must rejoin! 143 | def _in_kick(event) 144 | if event.target == bot_name 145 | # Rejoin almost immediately - logging is important. 146 | join event.channel 147 | end 148 | 149 | return true 150 | end 151 | 152 | # We're trying to join a channel - use key if we have one 153 | def _out_join(event) 154 | key = @passwords[event.channel] 155 | event.password.replace(key) unless key.to_s.empty? 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/net/yail/report_events.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module IRCEvents 3 | 4 | # This is the module for reporting a bunch of crap, included basically for legacy compatibility 5 | # and bots that need to be easy to use / debug right off the bat 6 | module Reports 7 | # Set up reporting filters - allows users who want it to keep reporting in their app relatively 8 | # easily while getting rid of it for everybody else 9 | def setup_reporting(yail) 10 | @yail = yail 11 | 12 | incoming_reporting = [ 13 | :msg, :act, :notice, :ctcp, :ctcpreply, :mode, :join, :part, :kick, 14 | :quit, :nick, :welcome, :bannedfromchan, :badchannelkey, :channelurl, :topic, 15 | :topicinfo, :endofnames, :motd, :motdstart, :endofmotd, :invite 16 | ] 17 | for event in incoming_reporting 18 | yail.after_filter(:"incoming_#{event}", self.method(:"r_#{event}") ) 19 | end 20 | 21 | outgoing_reporting = [ 22 | :msg, :act, :ctcp, :ctcpreply, :notice 23 | ] 24 | for event in outgoing_reporting 25 | yail.after_filter(:"outgoing_#{event}", self.method(:"r_out_#{event}") ) 26 | end 27 | 28 | generic_out_report = [ 29 | :join, :mode, :part, :quit, :nick, :user, :pass, :oper, :topic, :names, :list, :invite, :kick 30 | ] 31 | for event in generic_out_report 32 | yail.after_filter(:"outgoing_#{event}", self.method(:r_out_generic)) 33 | end 34 | end 35 | 36 | private 37 | def r_msg(event) 38 | @yail.log.info "{%s} <%s> %s" % [event.target || event.channel, event.nick, event.message] 39 | end 40 | 41 | def r_act(event) 42 | @yail.log.info "{%s} * %s %s" % [event.target || event.channel, event.nick, event.message] 43 | end 44 | 45 | def r_notice(event) 46 | nick = event.server? ? '' : event.nick 47 | @yail.log.info "{%s} -%s- %s" % [event.target || event.channel, nick, event.message] 48 | end 49 | 50 | def r_ctcp(event) 51 | @yail.log.info "{%s} [%s %s]" % [event.target || event.channel, event.nick, event.message] 52 | end 53 | 54 | def r_ctcpreply(event) 55 | @yail.log.info "{%s} [Reply: %s %s]" % [event.target || event.channel, event.nick, event.message] 56 | end 57 | 58 | def r_mode(event) 59 | @yail.log.info "{%s} %s sets mode %s %s" % [event.channel, event.from, event.message, event.targets.join(' ')] 60 | end 61 | 62 | def r_join(event) 63 | @yail.log.info "{#{event.channel}} #{event.nick} joins" 64 | end 65 | 66 | def r_part(event) 67 | @yail.log.info "{#{event.channel}} #{event.nick} parts (#{event.message})" 68 | end 69 | 70 | def r_kick(event) 71 | @yail.log.info "{#{event.channel}} #{event.nick} kicked #{event.target} (#{event.message})" 72 | end 73 | 74 | def r_quit(event) 75 | @yail.log.info "#{event.nick} quit (#{event.message})" 76 | end 77 | 78 | # Incoming invitation 79 | def r_invite(event) 80 | @yail.log.info "[#{event.nick}] INVITE to #{event.target}" 81 | end 82 | 83 | # Reports nick change unless nickname is us - we check nickname here since 84 | # the magic method changes @yail.me to the new nickname. 85 | def r_nick(event) 86 | @yail.log.info "#{event.nick} changed nick to #{event.message}" unless event.nick == @yail.me 87 | end 88 | 89 | def r_bannedfromchan(event) 90 | event.message =~ /^(\S*) :Cannot join channel/ 91 | @yail.log.info "Banned from channel #{$1}" 92 | end 93 | 94 | def r_badchannelkey(event) 95 | event.message =~ /^(\S*) :Cannot join channel/ 96 | @yail.log.info "Bad channel key (password) for #{$1}" 97 | end 98 | 99 | def r_welcome(event) 100 | @yail.log.info "*** Logged in as #{@yail.me}. ***" 101 | end 102 | 103 | # Channel URL 104 | def r_channelurl(event) 105 | event.message =~ /^(\S+) :?(.+)$/ 106 | @yail.log.info "{#{$1}} URL is #{$2}" 107 | end 108 | 109 | # Channel topic 110 | def r_topic(event) 111 | event.message =~ /^(\S+) :?(.+)$/ 112 | @yail.log.info "{#{$1}} Topic is: #{$2}" 113 | end 114 | 115 | # Channel topic setter 116 | def r_topicinfo(event) 117 | event.message =~ /^(\S+) (\S+) (\d+)$/ 118 | @yail.log.info "{#{$1}} Topic set by #{$2} on #{Time.at($3.to_i).asctime}" 119 | end 120 | 121 | # End of names 122 | def r_endofnames(event) 123 | event.message =~ /^(\S+)/ 124 | @yail.log.info "{#{$1}} Nickname list complete" 125 | end 126 | 127 | # MOTD line 128 | def r_motd(event) 129 | event.message =~ /^:?(.+)$/ 130 | @yail.log.info "*MOTD* #{$1}" 131 | end 132 | 133 | # Beginning of MOTD 134 | def r_motdstart(event) 135 | event.message =~ /^:?(.+)$/ 136 | @yail.log.info "*MOTD* #{$1}" 137 | end 138 | 139 | # End of MOTD 140 | def r_endofmotd(event) 141 | @yail.log.info "*MOTD* End of MOTD" 142 | end 143 | 144 | # Sent a privmsg (non-ctcp) 145 | def r_out_msg(event) 146 | @yail.log.info "{#{event.target}} <#{@yail.me}> #{event.message}" 147 | end 148 | 149 | # Sent a ctcp 150 | def r_out_ctcp(event) 151 | @yail.log.info "{#{event.target}} [#{@yail.me} #{event.message}]" 152 | end 153 | 154 | # Sent ctcp action 155 | def r_out_act(event) 156 | @yail.log.info "{#{event.target}} <#{@yail.me}> #{event.message}" 157 | end 158 | 159 | def r_out_notice(event) 160 | @yail.log.info "{#{event.target}} -#{@yail.me}- #{event.message}" 161 | end 162 | 163 | def r_out_ctcpreply(event) 164 | @yail.log.info "{#{event.target}} [Reply: #{@yail.me} #{event.message}]" 165 | end 166 | 167 | def r_out_generic(event) 168 | @yail.log.info "bot: #{event.inspect}" 169 | end 170 | end 171 | 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/net/yail/irc_bot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'net/yail' 3 | require 'net/yail/report_events' 4 | 5 | # My abstraction from adapter to a real bot. 6 | class IRCBot 7 | include Net::IRCEvents::Reports 8 | 9 | attr_reader :irc 10 | 11 | # Creates a new bot. Options are anything you can pass to the Net::YAIL constructor: 12 | # * :irc_network: Name/IP of the IRC server - backward-compatibility hack, and is 13 | # ignored if :address is passed in 14 | # * :address: Name/IP of the IRC server 15 | # * :port: Port number, defaults to 6667 16 | # * :username: Username reported to server 17 | # * :realname: Real name reported to server 18 | # * :nicknames: Array of nicknames to cycle through 19 | # * :throttle_seconds: Seconds between a cycle of privmsg sends. 20 | # Defaults to 1. One "cycle" is defined as sending one line of output to 21 | # *all* targets that have output buffered. 22 | # * :server_password: Very optional. If set, this is the password 23 | # sent out to the server before USER and NICK messages. 24 | # * :log: Optional, if set uses this logger instead of the default (Ruby's Logger). 25 | # If set, :loud and :silent options are ignored. 26 | # * :log_io: Optional, ignored if you specify your own :log - sends given object to 27 | # Logger's constructor. Must be filename or IO object. 28 | # * :use_ssl: Defaults to false. If true, attempts to use SSL for connection. 29 | def initialize(options = {}) 30 | @start_time = Time.now 31 | @options = options 32 | 33 | # Set up some friendly defaults 34 | @options[:address] ||= @options.delete(:irc_network) 35 | @options[:channels] ||= [] 36 | @options[:port] ||= 6667 37 | @options[:username] ||= 'IRCBot' 38 | @options[:realname] ||= 'IRCBot' 39 | @options[:nicknames] ||= ['IRCBot1', 'IRCBot2', 'IRCBot3'] 40 | end 41 | 42 | # Returns a string representing uptime 43 | def get_uptime_string 44 | uptime = (Time.now - @start_time).to_i 45 | seconds = uptime % 60 46 | minutes = (uptime / 60) % 60 47 | hours = (uptime / 3600) % 24 48 | days = (uptime / 86400) 49 | 50 | str = [] 51 | str.push("#{days} day(s)") if days > 0 52 | str.push("#{hours} hour(s)") if hours > 0 53 | str.push("#{minutes} minute(s)") if minutes > 0 54 | str.push("#{seconds} second(s)") if seconds > 0 55 | 56 | return str.join(', ') 57 | end 58 | 59 | # Creates the socket connection and registers the (very simple) default 60 | # welcome handler. Subclasses should build their hooks in 61 | # add_custom_handlers to allow auto-creation in case of a restart. 62 | def connect_socket 63 | @irc = Net::YAIL.new(@options) 64 | setup_reporting(@irc) 65 | 66 | # Simple hook for welcome to allow auto-joining of the channel 67 | @irc.on_welcome self.method(:welcome) 68 | 69 | add_custom_handlers 70 | end 71 | 72 | # To be subclassed - this method is a nice central location to allow the 73 | # bot to register its handlers before this class takes control and hits 74 | # the IRC network. 75 | def add_custom_handlers 76 | raise "You must define your handlers in add_custom_handlers, or else " + 77 | "explicitly override with an empty method." 78 | end 79 | 80 | # Enters the socket's listening loop(s) 81 | def start_listening 82 | # If socket's already dead (probably couldn't connect to server), don't 83 | # try to listen! 84 | if @irc.dead_socket 85 | $stderr.puts "Dead socket, can't start listening!" 86 | end 87 | 88 | @irc.start_listening 89 | end 90 | 91 | # Tells us the main app wants to just wait until we're done with all 92 | # thread processing, or get a kill signal, or whatever. For now this is 93 | # basically an endless loop that lets the threads do their thing until 94 | # the socket dies. If a bot wants, it can handle :irc_loop to do regular 95 | # processing. 96 | def irc_loop 97 | while true 98 | until @irc.dead_socket 99 | sleep 15 100 | @irc.dispatch Net::YAIL::CustomEvent.new(:type => :irc_loop) 101 | Thread.pass 102 | end 103 | 104 | # Disconnected? Wait a little while and start up again. 105 | sleep 30 106 | @irc.stop_listening 107 | self.connect_socket 108 | start_listening 109 | end 110 | end 111 | 112 | private 113 | # Basic handler for joining our channels upon successful registration 114 | def welcome(event) 115 | @options[:channels].each {|channel| @irc.join(channel) } 116 | end 117 | 118 | ################ 119 | # Helpful wrappers 120 | ################ 121 | 122 | # Wraps Net::YAIL.log 123 | def log 124 | @irc.log 125 | end 126 | 127 | # Wraps Net::YAIL.me 128 | def bot_name 129 | @irc.me 130 | end 131 | 132 | # Wraps Net::YAIL.msg 133 | def msg(*args) 134 | @irc.msg(*args) 135 | end 136 | 137 | # Wraps Net::YAIL.act 138 | def act(*args) 139 | @irc.act(*args) 140 | end 141 | 142 | # Wraps Net::YAIL.join 143 | def join(*args) 144 | @irc.join(*args) 145 | end 146 | 147 | # Logs the requested lines using the internal logger - deprecated to push logger on people 148 | def report(*lines) 149 | log.warn '[DEPRECATED] - IRCBot#report is deprecated and will be removed in 2.0 - use the logger (e.g., "@bot.log.info") instead' 150 | lines.each {|line| @irc.log.info line} 151 | end 152 | 153 | # Wraps Net::YAIL.nick 154 | def nick(*args) 155 | @irc.nick(*args) 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /tests/mock_irc.rb: -------------------------------------------------------------------------------- 1 | # An IO-like class that does IRC-like magic for us 2 | class MockIRC 3 | SERVER = "fakeirc.org" 4 | USERHOST = "fakey@nerdbucket.com" 5 | 6 | # Init - just call super and set up a couple vars 7 | def initialize(*args) 8 | super 9 | @connected = false 10 | @logged_in = false 11 | @closed = false 12 | @server = '' 13 | @output = [] 14 | @mutex = Mutex.new 15 | end 16 | 17 | # All output sent to the IO uses puts in YAIL. I hope. 18 | def puts(*args) 19 | @mutex.synchronize do 20 | for string in args 21 | handle_command(string.strip) 22 | end 23 | end 24 | return nil 25 | end 26 | 27 | # Lets us know if we are an open "socket" or not - if the socket is closed *and* we're out of 28 | # output, we are done 29 | def eof 30 | @mutex.synchronize do 31 | return @output.empty? && @closed 32 | end 33 | end 34 | 35 | alias_method :eof?, :eof 36 | 37 | # Pulls the first element off of @output 38 | def gets 39 | output = nil 40 | @mutex.synchronize do 41 | output = @output.shift 42 | end 43 | 44 | puts "WTF NO DATA!" if !output 45 | 46 | return output 47 | end 48 | 49 | # Hack for SSL mocking - it's expected tests will set the broken lines however we want them, so 50 | # this just spits out what gets would have 51 | def readpartial(size) 52 | data = gets 53 | return data || "" 54 | end 55 | 56 | # Hack to let YAIL know if we have data to read 57 | def ready? 58 | return @output.empty? ? nil : true 59 | end 60 | 61 | # All the magic goes here 62 | def handle_command(cmd) 63 | unless @connected 64 | handle_connected(cmd) 65 | return 66 | end 67 | 68 | unless @logged_in 69 | handle_nick(cmd) 70 | return 71 | end 72 | 73 | # TODO: Handle other commands 74 | case cmd 75 | when /^QUIT/ 76 | add_output ":#{SERVER} NOTICE #{@nick} :See ya, jerk" 77 | @closed = true 78 | 79 | when /^NICK/ then handle_nick(cmd) 80 | when /^JOIN/ then handle_join(cmd) 81 | when /^MODE/ then handle_mode(cmd) 82 | end 83 | end 84 | 85 | # Handles a connection command (USER) or errors 86 | def handle_connected(cmd) 87 | if cmd =~ /^USER (\S+) (\S+) (\S+) :(.*)$/ 88 | add_output ":#{SERVER} NOTICE AUTH :*** Looking up your hostname..." 89 | @connected = true 90 | return 91 | end 92 | 93 | add_output ":#{SERVER} ERROR :You need to authenticate or something" 94 | end 95 | 96 | # Dumb handling of a MODE command 97 | def handle_mode(cmd) 98 | # Eventually this could be used to make really interesting mode messages 99 | cmd =~ /^MODE(\s+\S+)?(\s+\S+)?(\s+\S+)?$/ 100 | 101 | add_output ":#{@nick}!#{USERHOST} MODE#{$2}#{$3}" 102 | end 103 | 104 | # Handles a NICK request, but no error if no nick set - not sure what a real server does here 105 | def handle_nick(cmd) 106 | unless cmd =~ /^NICK :(.*)$/ 107 | return 108 | end 109 | 110 | nick = $1 111 | 112 | if "InUseNick" == nick 113 | add_output ":#{SERVER} 433 * #{nick} :Nickname is already in use." 114 | return 115 | end 116 | 117 | oldnick = @nick 118 | @nick = nick 119 | 120 | unless @logged_in 121 | add_output "NOTICE AUTH :*** Looking up your hostname", 122 | ":#{SERVER} NOTICE #{@nick} :*** You are exempt from user limits. congrats.", 123 | ":#{SERVER} 001 #{@nick} :Welcome to the Fakey-fake Internet Relay Chat Network #{@nick}", 124 | ":#{SERVER} 002 #{@nick} :Your host is #{SERVER}[0.0.0.0/6667], running version mock-irc-1.7.7", 125 | ":#{SERVER} 003 #{@nick} :This server was created Nov 21 2009 at 21:20:48", 126 | ":#{SERVER} 004 #{@nick} #{SERVER} mock-irc-1.7.7 foobar barbaz bazfoo", 127 | ":#{SERVER} 005 #{@nick} CALLERID CASEMAPPING=rfc1459 DEAF=D KICKLEN=160 MODES=4 NICKLEN=15 PREFIX=(ohv)@%+ STATUSMSG=@%+ TOPICLEN=350 NETWORK=Fakeyfake MAXLIST=beI:25 MAXTARGETS=4 CHANTYPES=#& :are supported by this server", 128 | ":#{SERVER} 251 #{@nick} :There are 0 users and 24 invisible on 1 servers", 129 | ":#{SERVER} 254 #{@nick} 3 :channels formed", 130 | ":#{SERVER} 255 #{@nick} :I have 24 clients and 0 servers", 131 | ":#{SERVER} 265 #{@nick} :Current local users: 24 Max: 30", 132 | ":#{SERVER} 266 #{@nick} :Current global users: 24 Max: 33", 133 | ":#{SERVER} 250 #{@nick} :Highest connection count: 30 (30 clients) (4215 connections received)", 134 | ":#{SERVER} 375 #{@nick} :- #{SERVER} Message of the Day - ", 135 | ":#{SERVER} 372 #{@nick} :- BOO!", 136 | ":#{SERVER} 372 #{@nick} :- Did I scare you?", 137 | ":#{SERVER} 372 #{@nick} :- BOO again!", 138 | ":#{SERVER} 376 #{@nick} :End of /MOTD command." 139 | @logged_in = true 140 | return 141 | end 142 | 143 | # Hostmask is faked, but otherwise this is the message we send in response to a nick change 144 | add_output ":#{oldnick}!#{USERHOST} NICK :#{@nick}" 145 | end 146 | 147 | def handle_join(cmd) 148 | unless cmd =~ /^JOIN (\S*)( (\S*))?$/ 149 | return 150 | end 151 | 152 | channel = $1 153 | pass = $2 154 | if "#banned" == channel 155 | add_output ":#{SERVER} 474 #{@nick} #{channel}:Cannot join channel (+b)" 156 | return 157 | end 158 | 159 | add_output ":#{@nick}!#{USERHOST} JOIN :#{channel}", 160 | ":#{SERVER} 332 #{@nick} #{channel} :This is a topic, y'all!", 161 | ":#{SERVER} 333 #{@nick} #{channel} user!user@user.com 1291781166", 162 | ":#{SERVER} 353 #{@nick} = #{channel} :#{@nick} @Nerdmaster another_bot", 163 | ":#{SERVER} 366 #{@nick} #{channel} :End of /NAMES list." 164 | end 165 | 166 | # Sets up our internal string to add the given string arguments for a gets call to pull 167 | def add_output(*args) 168 | args.each {|arg| @output.push(arg + "\n")} 169 | end 170 | 171 | # Sets up our internal string to add a broken line (no newline character) 172 | def add_partial_output(string) 173 | @output.push(string) 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/net/yail/legacy_events.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module IRCEvents 3 | 4 | # All code here is going to be removed completely at some point, and only exists here to serve the 1.x branch 5 | # (and remind me how awful the old system really was) 6 | module LegacyEvents 7 | 8 | # DEPRECATED 9 | # 10 | # Event handler hook. Kinda hacky. Calls your event(s) before the default 11 | # event. Default stuff will happen if your handler doesn't return true. 12 | def prepend_handler(event, *procs, &block) 13 | raise "Cannot change handlers while threads are listening!" if @ioloop_thread 14 | 15 | unless $deprecated_prepend_warning 16 | @log.warn "[DEPRECATED] - Net::YAIL#prepend_handler is deprecated as of 1.5.0 - please see documentation on the new " + 17 | "event handling model methods - http://ruby-irc-yail.nerdbucket.com/" 18 | $deprecated_prepend_warning = true 19 | end 20 | 21 | # Allow blocks as well as procs 22 | if block_given? 23 | procs.push(block) 24 | end 25 | 26 | # See if this is a word for a numeric - only applies to incoming events 27 | if (event.to_s =~ /^incoming_(.*)$/) 28 | number = @event_number_lookup[$1].to_i 29 | event = :"incoming_numeric_#{number}" if number > 0 30 | end 31 | 32 | @legacy_handlers[event] ||= Array.new 33 | until procs.empty? 34 | @legacy_handlers[event].unshift(procs.pop) 35 | end 36 | end 37 | 38 | # Handles the given event (if it's in the @legacy_handlers array) with the 39 | # arguments specified. 40 | # 41 | # The @legacy_handlers must be a hash where key = event to handle and value is 42 | # a Proc object (via Class.method(:name) or just proc {...}). 43 | # This should be fine if you're setting up handlers with the prepend_handler 44 | # method, but if you get "clever," you're on your own. 45 | def handle(event, *arguments) 46 | # Don't bother with anything if there are no handlers registered. 47 | return false unless Array === @legacy_handlers[event] 48 | 49 | @log.debug "+++EVENT HANDLER: Handling event #{event} via #{@legacy_handlers[event].inspect}:" 50 | 51 | # Call all hooks in order until one breaks the chain. For incoming 52 | # events, we want something to break the chain or else it'll likely 53 | # hit a reporter. For outgoing events, we tend to report them anyway, 54 | # so no need to worry about ending the chain except when the bot wants 55 | # to take full control over them. 56 | result = false 57 | for handler in @legacy_handlers[event] 58 | result = handler.call(*arguments) 59 | break if result == true 60 | end 61 | 62 | # Let the new system deal with legacy handlers that wanted to end the chain 63 | return result 64 | end 65 | 66 | # Since numerics are so many and so varied, this method will auto-fallback 67 | # to a simple report if no handler was defined. 68 | def handle_numeric(number, fullactor, actor, target, message) 69 | # All numerics share the same args, and rarely care about anything but 70 | # message, so let's make it easier by passing a hash instead of a list 71 | args = {:fullactor => fullactor, :actor => actor, :target => target} 72 | base_event = :"incoming_numeric_#{number}" 73 | if Array === @legacy_handlers[base_event] 74 | return handle(base_event, message, args) 75 | else 76 | # No handler = report and don't worry about it 77 | @log.info "Unknown raw #{number.to_s} from #{fullactor}: #{message}" 78 | return false 79 | end 80 | end 81 | 82 | # Gets some input, sends stuff off to a handler. Yay. 83 | def legacy_process_event(event) 84 | # Allow global handler to break the chain, filter the line, whatever. When we ditch these legacy 85 | # events, this code will finally die! 86 | if (Net::YAIL::IncomingEvent === event && Array === @legacy_handlers[:incoming_any]) 87 | for handler in @legacy_handlers[:incoming_any] 88 | result = handler.call(event.raw) 89 | return true if true == result 90 | end 91 | end 92 | 93 | # Partial conversion to using events - we still have a horrible case statement, but 94 | # we're at least using the event object. Slightly less hacky than before. 95 | 96 | # Except for this - we still have to handle numerics the crappy way until we build the proper 97 | # dispatching of events 98 | event = event.parent if event.parent && :incoming_numeric == event.parent.type 99 | 100 | case event.type 101 | # Ping is important to handle quickly, so it comes first. 102 | when :incoming_ping 103 | return handle(event.type, event.message) 104 | 105 | when :incoming_numeric 106 | # Lovely - I passed in a "nick" - which, according to spec, is NEVER part of a numeric reply 107 | handle_numeric(event.numeric, event.from, nil, event.target, event.message) 108 | 109 | when :incoming_invite 110 | return handle(event.type, event.fullname, event.nick, event.channel) 111 | 112 | # Fortunately, the legacy handler for all five "message" types is the same! 113 | when :incoming_msg, :incoming_ctcp, :incoming_act, :incoming_notice, :incoming_ctcpreply 114 | # Legacy handling requires merger of target and channel.... 115 | target = event.target if event.pm? 116 | target = event.channel if !target 117 | 118 | # Notices come from server sometimes, so... another merger for legacy fun! 119 | nick = event.server? ? '' : event.nick 120 | return handle(event.type, event.from, nick, target, event.message) 121 | 122 | # This is a bit painful for right now - just use some hacks to make it work semi-nicely, 123 | # but let's not put hacks into the core Event object. Modes need reworking soon anyway. 124 | # 125 | # NOTE: message is currently the mode settings ('+b', for instance) - very bad. TODO: FIX FIX FIX! 126 | when :incoming_mode 127 | # Modes can come from the server, so legacy system again regularly sent nil data.... 128 | nick = event.server? ? '' : event.nick 129 | return handle(event.type, event.from, nick, event.channel, event.message, event.targets.join(' ')) 130 | 131 | when :incoming_topic_change 132 | return handle(event.type, event.fullname, event.nick, event.channel, event.message) 133 | 134 | when :incoming_join 135 | return handle(event.type, event.fullname, event.nick, event.channel) 136 | 137 | when :incoming_part 138 | return handle(event.type, event.fullname, event.nick, event.channel, event.message) 139 | 140 | when :incoming_kick 141 | return handle(event.type, event.fullname, event.nick, event.channel, event.target, event.message) 142 | 143 | when :incoming_quit 144 | return handle(event.type, event.fullname, event.nick, event.message) 145 | 146 | when :incoming_nick 147 | return handle(event.type, event.fullname, event.nick, event.message) 148 | 149 | when :incoming_error 150 | return handle(event.type, event.message) 151 | 152 | when :outgoing_privmsg, :outgoing_msg, :outgoing_ctcp, :outgoing_act, :outgoing_notice, :outgoing_ctcpreply 153 | return handle(event.type, event.target, event.message) 154 | 155 | when :outgoing_mode 156 | return handle(event.type, event.target, event.modes, event.objects) 157 | 158 | when :outgoing_join 159 | return handle(event.type, event.channel, event.password) 160 | 161 | when :outgoing_part 162 | return handle(event.type, event.channel, event.message) 163 | 164 | when :outgoing_quit 165 | return handle(event.type, event.message) 166 | 167 | when :outgoing_nick 168 | return handle(event.type, event.nick) 169 | 170 | when :outgoing_pass 171 | return handle(event.type, event.password) 172 | 173 | when :outgoing_oper 174 | return handle(event.type, event.user, event.password) 175 | 176 | when :outgoing_topic 177 | return handle(event.type, event.channel, event.topic) 178 | 179 | when :outgoing_names 180 | return handle(event.type, event.channel) 181 | 182 | when :outgoing_list 183 | return handle(event.type, event.channel, event.server) 184 | 185 | when :outgoing_invite 186 | return handle(event.type, event.nick, event.channel) 187 | 188 | when :outgoing_kick 189 | return handle(event.type, event.nick, event.channel, event.message) 190 | 191 | when :outgoing_begin_connection 192 | return handle(event.type, event.username, event.address, event.realname) 193 | end 194 | end 195 | end 196 | 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/net/yail/eventmap.yml: -------------------------------------------------------------------------------- 1 | # 001 ne 1 for the purpose of hash keying apparently. 2 | 001 : welcome 3 | 002 : yourhost 4 | 003 : created 5 | 004 : myinfo 6 | 005 : map # Undernet Extension, Kajetan@Hinner.com, 17/11/98 7 | 006 : mapmore # Undernet Extension, Kajetan@Hinner.com, 17/11/98 8 | 007 : mapend # Undernet Extension, Kajetan@Hinner.com, 17/11/98 9 | 008 : snomask # Undernet Extension, Kajetan@Hinner.com, 17/11/98 10 | 009 : statmemtot # Undernet Extension, Kajetan@Hinner.com, 17/11/98 11 | 010 : statmem # Undernet Extension, Kajetan@Hinner.com, 17/11/98 12 | 200 : tracelink 13 | 201 : traceconnecting 14 | 202 : tracehandshake 15 | 203 : traceunknown 16 | 204 : traceoperator 17 | 205 : traceuser 18 | 206 : traceserver 19 | 208 : tracenewtype 20 | 209 : traceclass 21 | 211 : statslinkinfo 22 | 212 : statscommands 23 | 213 : statscline 24 | 214 : statsnline 25 | 215 : statsiline 26 | 216 : statskline 27 | 217 : statsqline 28 | 218 : statsyline 29 | 219 : endofstats 30 | 220 : statsbline # UnrealIrcd, Hendrik Frenzel 31 | 221 : umodeis 32 | 222 : sqline_nick # UnrealIrcd, Hendrik Frenzel 33 | 223 : statsgline # UnrealIrcd, Hendrik Frenzel 34 | 224 : statstline # UnrealIrcd, Hendrik Frenzel 35 | 225 : statseline # UnrealIrcd, Hendrik Frenzel 36 | 226 : statsnline # UnrealIrcd, Hendrik Frenzel 37 | 227 : statsvline # UnrealIrcd, Hendrik Frenzel 38 | 231 : serviceinfo 39 | 232 : endofservices 40 | 233 : service 41 | 234 : servlist 42 | 235 : servlistend 43 | 241 : statslline 44 | 242 : statsuptime 45 | 243 : statsoline 46 | 244 : statshline 47 | 245 : statssline # Reserved, Kajetan@Hinner.com, 17/10/98 48 | 246 : statstline # Undernet Extension, Kajetan@Hinner.com, 17/10/98 49 | 247 : statsgline # Undernet Extension, Kajetan@Hinner.com, 17/10/98 50 | ### TODO: need numerics to be able to map to multiple strings 51 | ### 247 : statsxline # UnrealIrcd, Hendrik Frenzel 52 | 248 : statsuline # Undernet Extension, Kajetan@Hinner.com, 17/10/98 53 | 249 : statsdebug # Unspecific Extension, Kajetan@Hinner.com, 17/10/98 54 | 250 : luserconns # 1998-03-15 -- tkil 55 | 251 : luserclient 56 | 252 : luserop 57 | 253 : luserunknown 58 | 254 : luserchannels 59 | 255 : luserme 60 | 256 : adminme 61 | 257 : adminloc1 62 | 258 : adminloc2 63 | 259 : adminemail 64 | 261 : tracelog 65 | 262 : endoftrace # 1997-11-24 -- archon 66 | 265 : n_local # 1997-10-16 -- tkil 67 | 266 : n_global # 1997-10-16 -- tkil 68 | 271 : silelist # Undernet Extension, Kajetan@Hinner.com, 17/10/98 69 | 272 : endofsilelist # Undernet Extension, Kajetan@Hinner.com, 17/10/98 70 | 275 : statsdline # Undernet Extension, Kajetan@Hinner.com, 17/10/98 71 | 280 : glist # Undernet Extension, Kajetan@Hinner.com, 17/10/98 72 | 281 : endofglist # Undernet Extension, Kajetan@Hinner.com, 17/10/98 73 | 290 : helphdr # UnrealIrcd, Hendrik Frenzel 74 | 291 : helpop # UnrealIrcd, Hendrik Frenzel 75 | 292 : helptlr # UnrealIrcd, Hendrik Frenzel 76 | 293 : helphlp # UnrealIrcd, Hendrik Frenzel 77 | 294 : helpfwd # UnrealIrcd, Hendrik Frenzel 78 | 295 : helpign # UnrealIrcd, Hendrik Frenzel 79 | 300 : none 80 | 301 : away 81 | 302 : userhost 82 | 303 : ison 83 | 304 : rpl_text # Bahamut IRCD 84 | 305 : unaway 85 | 306 : nowaway 86 | 307 : userip # Undernet Extension, Kajetan@Hinner.com, 17/10/98 87 | 308 : rulesstart # UnrealIrcd, Hendrik Frenzel 88 | 309 : endofrules # UnrealIrcd, Hendrik Frenzel 89 | 310 : whoishelp # (July01-01)Austnet Extension, found by Andypoo 90 | 311 : whoisuser 91 | 312 : whoisserver 92 | 313 : whoisoperator 93 | 314 : whowasuser 94 | 315 : endofwho 95 | 316 : whoischanop 96 | 317 : whoisidle 97 | 318 : endofwhois 98 | 319 : whoischannels 99 | 320 : whoisvworld # (July01-01)Austnet Extension, found by Andypoo 100 | 321 : liststart 101 | 322 : list 102 | 323 : listend 103 | 324 : channelmodeis 104 | 328 : channelurl 105 | 329 : channelcreate # 1997-11-24 -- archon 106 | 331 : notopic 107 | 332 : topic 108 | 333 : topicinfo # 1997-11-24 -- archon 109 | 334 : listusage # Undernet Extension, Kajetan@Hinner.com, 17/10/98 110 | 335 : whoisbot # UnrealIrcd, Hendrik Frenzel 111 | 341 : inviting 112 | 342 : summoning 113 | 346 : invitelist # UnrealIrcd, Hendrik Frenzel 114 | 347 : endofinvitelist # UnrealIrcd, Hendrik Frenzel 115 | 348 : exlist # UnrealIrcd, Hendrik Frenzel 116 | 349 : endofexlist # UnrealIrcd, Hendrik Frenzel 117 | 351 : version 118 | 352 : whoreply 119 | 353 : namreply 120 | 354 : whospcrpl # Undernet Extension, Kajetan@Hinner.com, 17/10/98 121 | 361 : killdone 122 | 362 : closing 123 | 363 : closeend 124 | 364 : links 125 | 365 : endoflinks 126 | 366 : endofnames 127 | 367 : banlist 128 | 368 : endofbanlist 129 | 369 : endofwhowas 130 | 371 : info 131 | 372 : motd 132 | 373 : infostart 133 | 374 : endofinfo 134 | 375 : motdstart 135 | 376 : endofmotd 136 | 377 : motd2 # 1997-10-16 -- tkil 137 | 378 : austmotd # (July01-01)Austnet Extension, found by Andypoo 138 | 379 : whoismodes # UnrealIrcd, Hendrik Frenzel 139 | 381 : youreoper 140 | 382 : rehashing 141 | 383 : youreservice # UnrealIrcd, Hendrik Frenzel 142 | 384 : myportis 143 | 385 : notoperanymore # Unspecific Extension, Kajetan@Hinner.com, 17/10/98 144 | 386 : qlist # UnrealIrcd, Hendrik Frenzel 145 | 387 : endofqlist # UnrealIrcd, Hendrik Frenzel 146 | 388 : alist # UnrealIrcd, Hendrik Frenzel 147 | 389 : endofalist # UnrealIrcd, Hendrik Frenzel 148 | 391 : time 149 | 392 : usersstart 150 | 393 : users 151 | 394 : endofusers 152 | 395 : nousers 153 | 401 : nosuchnick 154 | 402 : nosuchserver 155 | 403 : nosuchchannel 156 | 404 : cannotsendtochan 157 | 405 : toomanychannels 158 | 406 : wasnosuchnick 159 | 407 : toomanytargets 160 | 408 : nosuchservice # UnrealIrcd, Hendrik Frenzel 161 | 409 : noorigin 162 | 411 : norecipient 163 | 412 : notexttosend 164 | 413 : notoplevel 165 | 414 : wildtoplevel 166 | 416 : querytoolong # Undernet Extension, Kajetan@Hinner.com, 17/10/98 167 | 421 : unknowncommand 168 | 422 : nomotd 169 | 423 : noadmininfo 170 | 424 : fileerror 171 | 425 : noopermotd # UnrealIrcd, Hendrik Frenzel 172 | 431 : nonicknamegiven 173 | 432 : erroneusnickname # This iz how its speld in thee RFC. 174 | 433 : nicknameinuse 175 | 434 : norules # UnrealIrcd, Hendrik Frenzel 176 | 435 : serviceconfused # UnrealIrcd, Hendrik Frenzel 177 | 436 : nickcollision 178 | 437 : bannickchange # Undernet Extension, Kajetan@Hinner.com, 17/10/98 179 | 438 : nicktoofast # Undernet Extension, Kajetan@Hinner.com, 17/10/98 180 | 439 : targettoofast # Undernet Extension, Kajetan@Hinner.com, 17/10/98 181 | 440 : servicesdown # Bahamut IRCD 182 | 441 : usernotinchannel 183 | 442 : notonchannel 184 | 443 : useronchannel 185 | 444 : nologin 186 | 445 : summondisabled 187 | 446 : usersdisabled 188 | 447 : nonickchange # UnrealIrcd, Hendrik Frenzel 189 | 451 : notregistered 190 | 455 : hostilename # UnrealIrcd, Hendrik Frenzel 191 | 459 : nohiding # UnrealIrcd, Hendrik Frenzel 192 | 460 : notforhalfops # UnrealIrcd, Hendrik Frenzel 193 | 461 : needmoreparams 194 | 462 : alreadyregistered 195 | 463 : nopermforhost 196 | 464 : passwdmismatch 197 | 465 : yourebannedcreep # I love this one... 198 | 466 : youwillbebanned 199 | 467 : keyset 200 | 468 : invalidusername # Undernet Extension, Kajetan@Hinner.com, 17/10/98 201 | 469 : linkset # UnrealIrcd, Hendrik Frenzel 202 | 470 : linkchannel # UnrealIrcd, Hendrik Frenzel 203 | 471 : channelisfull 204 | 472 : unknownmode 205 | 473 : inviteonlychan 206 | 474 : bannedfromchan 207 | 475 : badchannelkey 208 | 476 : badchanmask 209 | 477 : needreggednick # Bahamut IRCD 210 | 478 : banlistfull # Undernet Extension, Kajetan@Hinner.com, 17/10/98 211 | 479 : secureonlychannel # pircd 212 | ### TODO: see above todo 213 | ### 479 : linkfail # UnrealIrcd, Hendrik Frenzel 214 | 480 : cannotknock # UnrealIrcd, Hendrik Frenzel 215 | 481 : noprivileges 216 | 482 : chanoprivsneeded 217 | 483 : cantkillserver 218 | 484 : ischanservice # Undernet Extension, Kajetan@Hinner.com, 17/10/98 219 | 485 : killdeny # UnrealIrcd, Hendrik Frenzel 220 | 486 : htmdisabled # UnrealIrcd, Hendrik Frenzel 221 | 489 : secureonlychan # UnrealIrcd, Hendrik Frenzel 222 | 491 : nooperhost 223 | 492 : noservicehost 224 | 501 : umodeunknownflag 225 | 502 : usersdontmatch 226 | 511 : silelistfull # Undernet Extension, Kajetan@Hinner.com, 17/10/98 227 | 513 : nosuchgline # Undernet Extension, Kajetan@Hinner.com, 17/10/98 228 | 513 : badping # Undernet Extension, Kajetan@Hinner.com, 17/10/98 229 | 518 : noinvite # UnrealIrcd, Hendrik Frenzel 230 | 519 : admonly # UnrealIrcd, Hendrik Frenzel 231 | 520 : operonly # UnrealIrcd, Hendrik Frenzel 232 | 521 : listsyntax # UnrealIrcd, Hendrik Frenzel 233 | 524 : operspverify # UnrealIrcd, Hendrik Frenzel 234 | 235 | 600 : rpl_logon # Bahamut IRCD 236 | 601 : rpl_logoff # Bahamut IRCD 237 | 602 : rpl_watchoff # UnrealIrcd, Hendrik Frenzel 238 | 603 : rpl_watchstat # UnrealIrcd, Hendrik Frenzel 239 | 604 : rpl_nowon # Bahamut IRCD 240 | 605 : rpl_nowoff # Bahamut IRCD 241 | 606 : rpl_watchlist # UnrealIrcd, Hendrik Frenzel 242 | 607 : rpl_endofwatchlist # UnrealIrcd, Hendrik Frenzel 243 | 610 : mapmore # UnrealIrcd, Hendrik Frenzel 244 | 640 : rpl_dumping # UnrealIrcd, Hendrik Frenzel 245 | 641 : rpl_dumprpl # UnrealIrcd, Hendrik Frenzel 246 | 642 : rpl_eodump # UnrealIrcd, Hendrik Frenzel 247 | 248 | 999 : numericerror # Bahamut IRCD 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This document is not necessarily going to reflect the latest *stable* YAIL!** The latest stable release's 2 | documentation is always at [Nerdbucket.com](http://ruby-irc-yail.nerdbucket.com/). 3 | 4 | Net::YAIL is a library built for dealing with IRC communications in Ruby. 5 | This is a project I've been building on and off since 2005 or so, based 6 | originally on the very messy initial release of IRCSocket (back when I first 7 | started, that was the only halfway-decent IRC lib I found). I've put a lot 8 | of time and effort into cleaning it up to make it better for my own uses, 9 | and now it's almost entirely my code. 10 | 11 | Some credit should also be given to Ruby-IRC, as I stole its eventmap.yml 12 | file with very minor modifications. 13 | 14 | This library may not be useful to everybody (or anybody other than myself, 15 | for that matter), and Ruby-IRC or another lib may work for your situation 16 | far better than this thing will, but the general design I built here has 17 | just felt more natural to me than the other libraries I've looked at since 18 | I started my project. 19 | 20 | Example Usage 21 | ====== 22 | 23 | Need a slightly more complex example? For a separate project you can play 24 | with that relies on Net::YAIL, check out https://github.com/Nerdmaster/superloud 25 | (fair warning: this code is not even remotely appropriate!). 26 | 27 | For the nitty-gritty, you can see all this stuff in the [Net::YAIL docs](http://ruby-irc-yail.nerdbucket.com/) 28 | page, as well as more complete documentation about the system. For a complete bot, 29 | check out the IRCBot source code as well as the various examples found in the github 30 | project or in the gem's examples directory. Below is just a very simple example: 31 | 32 | ```ruby 33 | require 'rubygems' 34 | require 'net/yail' 35 | 36 | irc = Net::YAIL.new( 37 | :address => 'irc.someplace.co.uk', 38 | :username => 'Frakking Bot', 39 | :realname => 'John Botfrakker', 40 | :nicknames => ['bot1', 'bot2', 'bot3'] 41 | ) 42 | 43 | # Register a proc callback 44 | irc.on_welcome proc { |event| irc.join('#foo') } 45 | 46 | # Register a block 47 | irc.on_invite { |event| irc.join(event.channel) } 48 | 49 | # Another way to register a block - note that this clobbers the prior callback 50 | irc.set_callback(:incoming_invite) { |event| irc.join(event.channel) } 51 | 52 | # Filter for all incoming pings so we can log them 53 | irc.hearing_ping {|event| $stderr.puts event.inspect} 54 | 55 | # Filter condition: if the message is a pm, ignore it by forcibly ending the event filter chain 56 | irc.hearing_message(:if => {:pm? => true}) do |event| 57 | event.handle! 58 | end 59 | 60 | # Loops forever here until CTRL+C is hit. 61 | irc.start_listening! 62 | ``` 63 | 64 | Now we've built a simple IRC listener that will connect to a (probably 65 | invalid) network, identify itself, and sit around waiting for the welcome 66 | message. After this has occurred, we join a channel. If invited to another 67 | channel, we will join it. We spit out info about all incoming PINGs. 68 | 69 | Filters and callbacks: 70 | ============== 71 | 72 | YAIL is built with the concept of there being a single callback for any given 73 | event. Plugins can add functionality around an event via filters, but only 74 | the IRC client implementation should be writing callbacks. If you're building 75 | a bot or an IRC client, you should be handling events. If you're building 76 | a library that others will use and won't implement its own IRC handling, you 77 | should be primarily building filters. 78 | 79 | When a callback is set, it overwrites any previous callback. This allows sane 80 | defaults to be set up if they make sense (such as responding to a PING with a 81 | PONG), but if the user decides to do so, he can easily overwrite those 82 | defaults. 83 | 84 | Filters represent code that needs to be run before or after an event is 85 | handled. Filters running before an event can stop the event from triggering 86 | its callback, but this should be used only in very special cases (such as 87 | building a module to ignore events from specific users). Filters should be 88 | looked at as the hooks to be used when wanting to see an event, but shouldn't 89 | generally be the final callback of an event. 90 | 91 | Callback and filter methods: 92 | 93 | * set_callback(:xxx): Replaces the existing handler (if any) for the given event with the block or proc object passed 94 | in. Replace "xxx" with the callback name, such as :incoming_welcome, :outgoing_kick, etc. This is typically going 95 | to be used for incoming and custom events, but if you don't mind getting your hands dirty with raw IRC commands, 96 | you can also overwrite the outgoing handlers this way. 97 | * before_filter(:xxx), after_filter(:xxx): These create a filter for any event, and as above take a proc object or a 98 | block. As many filters as desired may be created for an event. A before_filter() call could be used to actually 99 | modify the data that gets sent to the callback, while an after_filter() would make more sense for something like 100 | logging or gathering stats only for events that make it through the callback. 101 | 102 | Shortcut methods make the common operations take a bit less typing, and are hopefully intuitive enough that you don't 103 | lose anything by using them. They are all used similarly to set_callback, before_filter, and after_filter, but with 104 | the event name as part of the method. They must be given a proc object or a block. 105 | 106 | * on_xxx: Sets a callback for an incoming event, so on_join will be the same as calling set_callback(:incoming_join) 107 | * hearing_xxx: Creates a before-filter on incoming event xxx. This is the same as calling before_filter(:incoming_xxx) 108 | * heard_xxx: Creates an after-filter on incoming event xxx. This is the same as calling after_filter(:incoming_xxx) 109 | * saying_xxx: Creates a before-filter on outgoing event xxx. This is the same as calling before_filter(:outgoing_xxx) 110 | * said_xxx: Creates an after-filter on outgoing event xxx. This is the same as calling after_filter(:outgoing_xxx) 111 | 112 | Conditional Filtering 113 | --------------------- 114 | 115 | For some situations, you want your filter to only be called if a certain condition is met. Enter conditional filtering! 116 | By using this exciting feature, you can set up handlers and callbacks which only trigger when certain conditions are 117 | met. Be warned, though, this can get confusing.... 118 | 119 | Conditions can be added to any filter method, but should **never** be used on the callback, since *there can be only one*. 120 | To add a filter, you simply supply a hash with a key of either `:if` or `:unless`, and a value which is either another 121 | hash of conditions, or a proc. 122 | 123 | If a proc is sent, it will be a method that is called and passed the event object. If the proc returns true, an `:if` 124 | condition is met and un `:unless` condition is not met. If a condition is not met, the filter is skipped entirely. 125 | 126 | If a hash is sent, each key is expected to be an attribute on the event object. It's similar to a lambda where you 127 | return true if each attribute equals the value in the hash. For instance, `:if => {:message => "food", :nick => "Simon"}` 128 | is the same as `:if => lambda {|e| e.message == "food" && e.nick == "Simon"}`. 129 | 130 | Example (very simple) filter conditions: 131 | 132 | ```ruby 133 | ### 134 | # NOTE: These were swiped from tests - "hearing_food", "not_hearing_bad", and "heard_nothing" are all lambda functions 135 | ### 136 | 137 | # If the message looks like "food", the handler will be hit 138 | @irc.hearing_msg(hearing_food, :if => lambda {|e| e.message =~ /food/}) 139 | 140 | # Unless the message looks like "bad", the handler will be hit 141 | @irc.hearing_msg(not_hearing_bad, :unless => lambda {|e| e.message =~ /bad/}) 142 | 143 | # If the message is completely empty, this handler will be hit 144 | @irc.heard_msg(heard_nothing, :if => {:message => ""}) 145 | 146 | # Multiple handler conditions are "ANDed" - that is they must all be true for the :if to succeed or the :unless to 147 | # fail. We do a block here instead of a proc to see how it looks. 148 | @irc.heard_msg(:if => {:message => "bah", :pm? => true}) do 149 | # Message was "bah" and a pm! 150 | end 151 | ``` 152 | 153 | Features of YAIL: 154 | ======== 155 | 156 | * Allows event callbacks to be specified very easily for all known IRC events, 157 | and in all cases, one can choose to override the default handling mechanisms. 158 | Generally speaking, it's best to be sure you know what you're doing when you 159 | decide to change how PING is responded to, but the capability is there. 160 | * Allows handling outgoing messages, such as when privmsg is called. You can 161 | filter data before it's sent out, log statistics after it's sent, or even 162 | customize the raw socket output. This is one feature I didn't see anywhere 163 | else. 164 | * Threads for input and output are persistent. This is a feature, not a bug. 165 | Some may hate this approach, but I'm a total n00b to threads, and it seemed 166 | like the way to go, having thread loops responsible for their own piece of 167 | the library. I'd *love* input here if anybody can tell me why this is a bad 168 | idea.... 169 | * Unlimited before- and after-callback filters allow for building a modular 170 | framework on top of YAIL. 171 | * There is now only ONE callback per event as of YAIL 1.5 (2.0 will actually 172 | remove the code supporting the "legacy" event system). This is a bit more 173 | constrictive than some libraries, but makes it a lot more clear what is the 174 | definitive handler of an event versus what provides functionality separate 175 | from said handler. For simple bots, this should actually be easier to use. 176 | * Easy to build a simple bot without subclassing anything. One gripe I had 177 | with IRCSocket was that it was painful to do anything without subclassing 178 | and overriding methods. No need here. 179 | * Lots of built-in reporting comes free by subclassing IRCBot, but is no longer 180 | required otherwise. 181 | * Built-in PRIVMSG buffering! You can of course choose to not buffer, but by 182 | default you cannot send more than one message to a given target (user or 183 | channel) more than once per second. Additionally, this buffering method is 184 | ideal for a bot that's trying to be chatty on two channels at once, because 185 | buffering is per-target, so queing up 20 lines on ##foo doesn't mean waiting 186 | 20 seconds to spit data out to ##bar. The one caveat here is that if your 187 | app is trying to talk to too many targets at once, the buffering still won't 188 | save you from a flood-related server kick. If this is a problem for others, 189 | I'll look into building an even more awesome buffering system. 190 | * The included IRCBot is a great starting point for building your own bot, 191 | but if you want something even simpler, just look at Net::YAIL's documentation 192 | for the most basic working examples. 193 | 194 | I still have a lot to do, though. The output API is definitely not fully 195 | fleshed out. I believe that the library is also missing a lot for people 196 | who just have a different approach than me, since this was purely designed for 197 | my own benefit, and then released almost exclusively to piss off the people 198 | whose work I stole to get where I'm at today. (Just kiddin', Pope) 199 | 200 | This code is released under the MIT license. I hear it's all the rage with 201 | the kids these days. 202 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.6.2 2 | ===== 3 | 4 | SSL fixed! Again. Yeah, so by "fixed", I probably mean to say, "I am a moron and some new bug will 5 | surely pop up again soon." 6 | 7 | If you use SSL regularly, let me know how THIS fix goes.... 8 | 9 | (On the plus side, this time I tested it a little) 10 | 11 | 1.6 12 | === 13 | 14 | This is primarily a bugfix release, but it's been so long since 1.5 that people will think I've 15 | done a ton of work if I call it 1.6. Plus there's one minor new feature that doesn't suck. 16 | 17 | New 18 | --- 19 | 20 | * Default documentation output is now sdoc, which is nice, instead of the new "darkfish" garbage 21 | theme rdoc uses by default. Not to be a jerk, but WOW, GUYS. Really, *wow*. 22 | * Seriously, I even provided a patch to sdoc to make it work better for me because I hated that 23 | theme so very much. 24 | * CONDITIONAL FILTERS OMFG - see README for info here, but basically this allows an `:if` or 25 | `:unless` to be specified for filters 26 | 27 | Changes 28 | ------- 29 | 30 | * Loudbot removed completely due to awesome level of inappropriate code. The standalone project 31 | is still referenced in the README, so if you love SUPERLOUD (and I know you do), it's still easy 32 | to find the source. 33 | * Better testing to ensure a more wholesome, quality product for you *and* your loved ones 34 | * All dispatch functionality was moved into its own module in an attempt to make the core yail.rb 35 | file feel cleaner, and try (in vain) to separate the YAIL library's concerns out slightly. 36 | 37 | Fixes 38 | ----- 39 | 40 | * Various Ruby and Rake compatibility changes 41 | * No more bazillions of warnings on use of deprecated prepend_handler method. Yay. 42 | * A little bit of back-end clean-up to make future changes easier 43 | * Using "unknown" outgoing events no longer causes a crash (DOH) 44 | * Trying to use SSL should not *crash* anymore (BIGGER DOH) 45 | * There's like a fix to make stuff suck less on Ruby 1.9.x and it's totally rad 46 | 47 | 48 | Version 1.5 49 | =========== 50 | 51 | API changes: 52 | ------------ 53 | 54 | Deprecating prepend_handler in favor of before_filter, set_callback, and after_filter. 55 | prepend_handler and current event system stay in for a while, but won't last forever.... 56 | 57 | Related to above: callback and filters must very explicitly break the chain, and it should 58 | only be done in very specific situations. 59 | 60 | Callbacks should be looked at as the core code, the response to an action. They 61 | should be used to actually handle an event: a bot's "PRIVMSG #channel Hey there, bob" output on 62 | a "bob joins the channel" message. Filters are more for behind-the-scenes stuff: responding to a 63 | PING, storing stats, logging, manipulating input/output, changing variables (bot's nickname on an 64 | :incoming_nick event), etc. 65 | 66 | A plugin could use a filter where one isn't really appropriate, and this may make sense for a 67 | plugin built for a very specific purpose - but the *final* program should be the only thing that 68 | registers the callback, because there can be only one. When you call set_callback, any previous 69 | callback is lost, PERIOD. 70 | 71 | Looking at this like Rails, the callback would be the action, while filters would be... filters. 72 | Looking at it like wordpress, the callback is the core WP code, while the filters are your plugin 73 | actions. One plugin author shouldn't be breaking another's ability to work. Again, exceptions to 74 | every rule: for instance, an "ignore user" plugin could stop an event from ever reaching the 75 | callback. 76 | 77 | 1.5 issues 78 | ---------- 79 | 80 | Though I got it working fairly well, the legacy support isn't 100% compatible with 1.4.3: 81 | 82 | * Reporting is now a separate module you have to include manually (or subclass IRCBot) 83 | * Outgoing events have been modified heavily. They are now callbacks instead of "core" 84 | functionality, and may run differently than expected. For instance, you used to be unable to 85 | override the core output of a PRIVMSG; now you can. You used to not be able to "break the chain" 86 | before an outgoing PRIVMSG command; now you can. To keep legacy funcationality similar to how it 87 | worked previously, legacy events using the prepend_handler system will *not* be able to override 88 | or break the chain for outgoing events. But this comes at the cost of things potentially being 89 | out of order from what you're used to if you're trying to mix legacy handlers and new filters. 90 | The reporting module is, of course, using the new filters, so they may act strangely. 91 | * The output API should be exactly the same as it used to be, so for a simple app, you may not 92 | notice the difference. 93 | * Generally speaking, mixing legacy and new handlers will result in interesting behaviors. Try to 94 | update your app to use the new system if you want the full capabilities of the new API. 95 | 96 | How to prepare your code for 2.0: 97 | --------------------------------- 98 | 99 | * Convert all handlers to callbacks or filters as described in the documentation (latest stable 100 | version's docs are always available at http://ruby-irc-yail.nerdbucket.com/) 101 | * Use on_xxx, hearing_xxx, heard_xxx, saying_xxx, and said_xxx if they simplify your code 102 | * Handlers need only take a single parameter now, the event object itself 103 | * Use the documentation to determine event methods available for a given event. In a pinch, you can use this to help see what's available: 104 | $stderr.puts "Events of type #{event.type} have the following methods: #{event.public_methods(false).inspect}" 105 | * Remove all direct calls to handle(). For custom events, use a call to dispatch() with a Net::YAIL::CustomEvent.new(:type => :blah, ...) 106 | * When you need to break the event handling chain, call "event.handled!". This name is meant to 107 | alert you that it has side effects and you usually shouldn't need to call it now that reporting 108 | is no longer forced on you so painfully. 109 | * Note that return values from event handlers no longer hold ANY meaning 110 | 111 | Anything not well-documented, confusing, or done wrong? Yell at me on github and maybe I'll fix it. 112 | 113 | 1.4.6 114 | ===== 115 | 116 | The from data is always present on all events, even if it has to be blank, in order to accommodate 117 | the edge cases where having data is expected, but it isn't there: 118 | NOTICE :ERROR from foo.bar.com 119 | 120 | 1.4.5 121 | ===== 122 | 123 | Pulled fix for RFC 1459 compliance - newline should be CR-LF. 124 | 125 | 1.4.4 126 | ===== 127 | 128 | SSL support has been enhanced by code I don't fully understand, but which a helpful user suggested 129 | could fix the one-message-behind situation! 130 | 131 | 1.4.3 132 | ===== 133 | 134 | New 135 | --- 136 | 137 | * Support for SSL! This is highly experimental as I know nothing of SSL. I'm literally trying to 138 | steal the code from Ruby-IRC that does SSL and make it work with YAIL. So far it doesn't seem to 139 | be working quite right - it's always one message behind for some reason. This means on a channel 140 | with activity, it'll respond to events one message late. On a channel without much activity, it 141 | can in fact miss pings and eventually is kicked off. Help me! 142 | 143 | 1.4.2 144 | ===== 145 | 146 | New 147 | --- 148 | 149 | * YAIL now defaults to use Logger for all non-event logging. Prepare for all logging to be pulled 150 | out of YAIL soon, but anything that stays core will be *real* logs (debug logs for IRC messages, 151 | fatal for truly critical errors, etc) instead of just STDERR prints. You can access the log via 152 | yail_object.log in order to change loglevel and such, and pass in your own logger via YAIL's 153 | constructor. 154 | 155 | Changes 156 | ------- 157 | 158 | * Specifying :loud and :silent in the constructor are NOW DEPRECATED. These will be removed by 159 | version 2, and possibly at the next minor version bump. I hate them. Instead, either specify 160 | your own logger via the :log key in the constructor, or change your object via something like 161 | `irc.log.level = ...` 162 | * @loud and @silent have been renamed to discourage using those directly as a way to avoid the 163 | deprecation warnings. I will change these again, so don't rely on them! 164 | * Note that if you really hate the warnings, you can of course choose not to hear them - 165 | they're logged as WARN level events. 166 | * The default level will be WARN. The "loud" level is DEBUG. The "silent" level is FATAL. 167 | * Note that YAIL still uses @silent for suppressing event reports in the output API. This is 168 | because I'm a moron, and I need to keep that functionality as-is until I tear it out of the 169 | library completely. Some people who are really stupid (me) actually rely on that reporting. 170 | When I tear this stuff out is when deprecation ends and full removal of those options happens. 171 | * Basic bot class is now in the file net/yail/irc_bot instead of net/yail/IRCBot, to better follow 172 | Ruby conventions. I've left IRCBot there for now, but I may eventually take that away, so 173 | consider fixing your `requires` statements today! 174 | 175 | 176 | 1.4.1 177 | ===== 178 | 179 | Had some screwups here before deploying the gem, so this version was removed 180 | 181 | 1.4.0 182 | ===== 183 | 184 | New 185 | --- 186 | 187 | * HOLY CRAP! I never provided a topic change event?!? Well, now it's here. :incoming_topic_change 188 | at your service. Note that this is VERY DIFFERENT from the numeric :incoming_topic event, which 189 | only tells you current topic for a channel. 190 | * NOTE: if you've been handling topic changes via the miscellaneous handler, you *will* have to 191 | change your code! 192 | * prepend_handler can now take a block instead of just a Proc object! 193 | * In addition to using the `start_listening` method of Net::YAIL, you can now call the "dangerous" 194 | version of that method: `start_listening!`, which wraps the "safe" version, but also starts an 195 | endless loop. For extremely simple bots, this is simplifies your codebase greatly. 196 | * New bot example added to demonstrate a very simple case that's still pretty easy to extend and 197 | configure: examples/simple/dumbbot.rb 198 | 199 | Changes 200 | ------- 201 | 202 | * CTRL+C termination now results in a graceful termination which includes sending QUIT to the server 203 | * Major event handling overhaul. Legacy apps *should* work fine, but if not, please let me know 204 | by filing a ticket on github or something! 205 | * For those who care, the back-end is now using a new class for handling and storing events. 206 | This class is eventually going to be used for all handlers, rather than passing around a bunch 207 | of arguments that are often unused. 208 | * README was removed in favor of two files: YAIL-RDOC for the rdoc "intro" info, and README.md for 209 | a github-friendly description of the bot. 210 | 211 | Fixes 212 | ----- 213 | 214 | * Nickname change in forced handler is now safer 215 | * Version information is now stored outside the main YAIL library 216 | * TODO updated a lot to better reflect my short-, medium-, and long-term goals 217 | * General update to documentation - a few grammatical fixes, some doc errors fixed, etc 218 | * MessageParser fixes to make it better at handling some of the edge cases, and better tests to 219 | make sure it's doing what I think it's doing. 220 | 221 | 1.3.5 and prior 222 | =============== 223 | 224 | Go to github, look at the incredibly long and painful old version of CHANGELOG. Weep. Hate 225 | Nerdmaster for being such a moron. 226 | -------------------------------------------------------------------------------- /lib/net/yail/event.rb: -------------------------------------------------------------------------------- 1 | require 'net/yail/message_parser.rb' 2 | 3 | module Net 4 | class YAIL 5 | 6 | # Base event class for stuff shared by any type of event. Note that :type and :handled 7 | # apply to *all* events, so they're explicitly defined here. 8 | class BaseEvent 9 | # Creates an event object and sets up some sane defaults for common elements. Any elements 10 | # in the data hash are converted to "magic" methods. 11 | def initialize(data = {}) 12 | # Don't modify incoming data! 13 | @data = data.dup 14 | @handled = false 15 | @type = @data.delete(:type) 16 | 17 | # All events have the capacity for a parent 18 | @data[:parent] ||= nil 19 | 20 | # Give useful accessors in a hacky but fun way! I can't decide if I prefer the pain of 21 | # using method_missing or the pain of not knowing how to do this without a string eval.... 22 | for key in @data.keys 23 | key = key.to_s 24 | self.instance_eval("def #{key}; return @data[:#{key}]; end") 25 | end 26 | 27 | raise "BaseEvent not usable - please subclass" if BaseEvent == self.class 28 | end 29 | 30 | # Helps us debug 31 | def to_s 32 | return super().gsub(self.class.name, "%s [%s]" % [self.class.name, @type.to_s]) 33 | end 34 | 35 | # Slightly unintuitive name to avoid accidental use - we don't want it to be the norm to stop 36 | # the event handling chain anymore! Filters + callback should make that a rarity. 37 | def handled!; @handled = true; end 38 | 39 | # Cheesy shortcut to @handled in "boolean" form 40 | def handled?; return @handled; end 41 | 42 | def event_class 43 | return "base" 44 | end 45 | 46 | def type 47 | return ("%s_%s" % [event_class, @type]).to_sym 48 | end 49 | end 50 | 51 | # The outgoing event class - outgoing events haven't got much in 52 | # common, so this class is primarily to facilitate the new system. 53 | class OutgoingEvent < BaseEvent 54 | # Outgoing events in our system are always :outgoing_xxx 55 | def event_class 56 | return "outgoing" 57 | end 58 | end 59 | 60 | # Custom event is just a base event that doesn't crash when accessing type :) 61 | class CustomEvent < BaseEvent 62 | def event_class; return "custom"; end 63 | def type; return @type; end 64 | end 65 | 66 | # This is the incoming event class. For all situations where the server 67 | # sent us some kind of event, this class handles all the data. 68 | # 69 | # All events will have a :raw attribute that stores the exact text sent from 70 | # the IRC server. Other possible pieces of data are as follows: 71 | # * fullname: Rarely needed, full text of origin of an action 72 | # * nick: Nickname of originator of an event 73 | # * from: Nickname *or* server name, should be on every event 74 | # * channel: Where applicable, the name of the channel in which the event 75 | # happened. 76 | # * message: Actual message/emote/notice/etc 77 | # * target: User targeted for various commands - PRIVMSG/NOTICE recipient, KICK victim, etc 78 | # * pm?: Set to true if the event is a "private" event (not sent to the 79 | # channel). Useful primarily for message types of events (PRIVMSG). 80 | # 81 | # To more easily call the right user event, we store each event type and its "parent" where it 82 | # makes sense. This ensures that a user currently handling :incoming_ctcp won't be totally 83 | # screwed when we add in :incoming_userinfo and such. Top-level handlers aren't in here, which 84 | # is vital to avoid trying to hack around numerics (not to mention a bunch of fairly useless 85 | # data) 86 | # 87 | # Look at the source for specifics of which IRC events set up what data. Or try to parse the 88 | # lovely RFCs.... 89 | # 90 | # For convenience, the event stores its MessageParser object so users can access raw data as 91 | # necessary (for numeric messages, this is often useful) 92 | class IncomingEvent < BaseEvent 93 | attr_reader :raw, :msg 94 | private_class_method :new 95 | 96 | # Incoming events always have :raw and :msg in the data hash 97 | def initialize(data = {}) 98 | # Don't modify incoming element! 99 | @data = data.dup 100 | @raw = @data.delete(:raw) 101 | @msg = @data.delete(:msg) 102 | 103 | super(data) 104 | end 105 | 106 | # Incoming events in our system are always :incoming_xxx 107 | def event_class 108 | return "incoming" 109 | end 110 | 111 | # Effectively our event "factory" - uses Net::YAIL::MessageParser and returns an event 112 | # object - usually just one, but TODO: some lines actually contain multiple messages. When 113 | # EventManager or similar is implemented, we'll just register events and this will be a non-issue 114 | def self.parse(line) 115 | # Parse with MessageParser to get raw IRC info 116 | raw = line.dup 117 | msg = Net::YAIL::MessageParser.new(line) 118 | 119 | # All incoming events need .raw and .msg in addition to any base event attributes. 120 | # 121 | # "from" is a tricky case as it isn't used on all messages - but because it's something of 122 | # a standard we rely on for so many messages, it has a default so that at the least one can 123 | # rely on not getting a crash for some of the edge cases (like "NOTICE :ERROR from foo.bar.com" 124 | # or a server-less "NOTICE AUTH :xxxx"). Maybe more elements should have defaults... not 125 | # real sure yet. 126 | data = { :raw => raw, :msg => msg, :from => nil } 127 | 128 | # Not all messages from the server identify themselves as such, so we just assume it's from 129 | # the server unless we explicitly see a nick 130 | data[:server?] = true 131 | 132 | # Sane defaults for most messages 133 | if msg.servername 134 | data[:from] = data[:servername] = msg.servername 135 | elsif msg.prefix && msg.nick 136 | data[:fullname] = msg.prefix 137 | data[:from] = data[:nick] = msg.nick 138 | data[:server?] = false 139 | end 140 | 141 | case msg.command 142 | when 'ERROR' 143 | data[:type] = :error 144 | data[:message] = msg.params.last 145 | event = new(data) 146 | 147 | when 'PING' 148 | data[:type] = :ping 149 | data[:message] = msg.params.last 150 | event = new(data) 151 | 152 | when 'TOPIC' 153 | data[:type] = :topic_change 154 | data[:channel] = msg.params.first 155 | data[:message] = msg.params.last 156 | event = new(data) 157 | 158 | when /^\d{3}$/ 159 | # Get base event for the "numeric" type - so many of these exist, and so few are likely 160 | # to be handled directly. Sadly, some hackery has to happen here to make "message" backward- 161 | # compatible since old YAIL auto-joined all parameters into one string. 162 | data[:type] = :numeric 163 | params = msg.params.dup 164 | data[:target] = params.shift 165 | data[:parameters] = params 166 | data[:message] = params.join(' ') 167 | data[:numeric] = msg.command.to_i 168 | event = new(data) 169 | 170 | # Create child event for the specific numeric 171 | data[:type] = :"numeric_#{msg.command.to_i}" 172 | data[:parent] = event 173 | event = new(data) 174 | 175 | when 'INVITE' 176 | data[:type] = :invite 177 | data[:channel] = msg.params.last 178 | 179 | # This should always be us, but still worth capturing just in case 180 | data[:target] = msg.params.first 181 | event = new(data) 182 | 183 | # This can encompass three possible messages, so further refining happens here - the last param 184 | # is always the message itself, so we look for patterns there. 185 | when 'PRIVMSG' 186 | event = privmsg_events(msg, data) 187 | 188 | # This can encompass two possible messages, again based on final param 189 | when 'NOTICE' 190 | event = notice_events(msg, data) 191 | 192 | when 'MODE' 193 | event = mode_events(msg, data) 194 | 195 | when 'JOIN' 196 | data[:type] = :join 197 | data[:channel] = msg.params.last 198 | event = new(data) 199 | 200 | when 'PART' 201 | data[:type] = :part 202 | data[:channel] = msg.params.first 203 | data[:message] = msg.params.last 204 | event = new(data) 205 | 206 | when 'KICK' 207 | data[:type] = :kick 208 | data[:channel] = msg.params[0] 209 | data[:target] = msg.params[1] 210 | data[:message] = msg.params[2] 211 | event = new(data) 212 | 213 | when 'QUIT' 214 | data[:type] = :quit 215 | data[:message] = msg.params.first 216 | event = new(data) 217 | 218 | when 'NICK' 219 | data[:type] = :nick 220 | data[:message] = msg.params.first 221 | event = new(data) 222 | 223 | # Unknown line! If this library is complete, we should *never* see this situation occur, 224 | # so it'll be up to the caller to decide what to do. 225 | else 226 | data[:type] = :unknown 227 | event = new(data) 228 | end 229 | 230 | return event 231 | end 232 | 233 | protected 234 | 235 | # Parses a MODE to its events - basic, backward-compatible :mode event for now, but 236 | # TODO: eventually get set up for multiple atomic mode messages (need event manager first) 237 | def self.mode_events(msg, data) 238 | data[:type] = :mode 239 | data[:channel] = msg.params.shift 240 | data[:message] = msg.params.shift 241 | data[:targets] = msg.params 242 | event = new(data) 243 | end 244 | 245 | # Parses basic data for the "message" constructs: PRIVMSG and NOTICE 246 | def self.parse_message_data(msg, data) 247 | # Defaults so all messages have a fairly standard interface 248 | data[:pm?] = false 249 | data[:target] = nil 250 | data[:channel] = msg.params.first 251 | 252 | # If this isn't a channel message, set up PM data - keep channel, just set it to nil so the 253 | # API is consistent 254 | unless msg.params.first =~ /^[!&#+]/ 255 | data[:channel] = nil 256 | data[:pm?] = true 257 | data[:target] = msg.params.first 258 | end 259 | end 260 | 261 | # Parses a PRIVMSG to its events - CTCP stuff needs parents, ACT stuff needs two-parent 262 | # hierarchy 263 | def self.privmsg_events(msg, data) 264 | # Parse common elements 265 | parse_message_data(msg, data) 266 | 267 | # Get base event 268 | data[:type] = :msg 269 | data[:message] = msg.params.last 270 | event = new(data) 271 | 272 | # Is this CTCP? 273 | if event.message =~ /^\001(.+?)\001$/ 274 | data[:type] = :ctcp 275 | data[:message] = $1 276 | data[:parent] = event 277 | 278 | event = new(data) 279 | end 280 | 281 | # CTCP action? 282 | if :ctcp == data[:type] && event.message =~ /^ACTION (.+)$/ 283 | data[:type] = :act 284 | data[:message] = $1 285 | data[:parent] = event 286 | 287 | event = new(data) 288 | end 289 | 290 | return event 291 | end 292 | 293 | # Parses a NOTICE to its events - CTCP replies come through here 294 | def self.notice_events(msg, data) 295 | # Parse common elements 296 | parse_message_data(msg, data) 297 | 298 | # Get base event 299 | data[:type] = :notice 300 | data[:message] = msg.params.last 301 | event = new(data) 302 | 303 | if event.message =~ /^\001(.+?)\001$/ 304 | data[:type] = :ctcp_reply 305 | data[:message] = $1 306 | data[:parent] = event 307 | 308 | event = new(data) 309 | end 310 | 311 | return event 312 | end 313 | 314 | end 315 | end 316 | end 317 | -------------------------------------------------------------------------------- /tests/tc_event.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/net_yail' 3 | require 'test/unit' 4 | 5 | # Stolen from tc_message_parser - same tests, different object 6 | class MessageParserEventTest < Test::Unit::TestCase 7 | # Simplest test case, I think 8 | def test_ping 9 | event = Net::YAIL::IncomingEvent.parse("PING :nerdbucket.com") 10 | assert_equal 'nerdbucket.com', event.message 11 | assert_equal :incoming_ping, event.type 12 | assert !event.respond_to?(:servername) 13 | assert !event.respond_to?(:nick) 14 | assert !event.respond_to?(:channel) 15 | assert !event.respond_to?(:fullname) 16 | assert event.server? 17 | end 18 | 19 | def test_topic 20 | event = Net::YAIL::IncomingEvent.parse(":\"Dudeföö!dude@nerdbucket.com TOPIC #nerdtalk :31 August 2010 \357\277\275 Foo.") 21 | assert_equal :incoming_topic_change, event.type 22 | assert_equal '"Dudeföö', event.nick 23 | assert_equal "31 August 2010 \357\277\275 Foo.", event.message 24 | assert_equal '#nerdtalk', event.channel 25 | assert_equal '"Dudeföö!dude@nerdbucket.com', event.fullname 26 | end 27 | 28 | # Parsing of PRIVMSG messages 29 | def test_messages 30 | # Basic test of privmsg-type command 31 | event = Net::YAIL::IncomingEvent.parse(':Nerdmaster!jeremy@nerdbucket.com PRIVMSG Nerdminion :Do my bidding!!') 32 | assert_nil event.parent 33 | assert_equal 'Nerdmaster', event.nick 34 | assert_equal 'jeremy', event.msg.user 35 | assert_equal 'nerdbucket.com', event.msg.host 36 | assert_equal 'Nerdmaster!jeremy@nerdbucket.com', event.fullname 37 | assert_equal 'Nerdmaster', event.from 38 | assert !event.server? 39 | assert_equal 'PRIVMSG', event.msg.command 40 | assert_equal :incoming_msg, event.type 41 | assert_equal 'Nerdminion', event.target 42 | assert_equal true, event.pm? 43 | assert_equal 'Do my bidding!!', event.message 44 | 45 | # CTCP to user 46 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com PRIVMSG Nerdminion :\001FOO is to bar as BAZ is to...?\001") 47 | assert_equal 'Nerdmaster', event.nick 48 | assert_equal :incoming_ctcp, event.type 49 | assert_nil event.channel 50 | assert_equal 'Nerdminion', event.target 51 | assert_equal true, event.pm? 52 | assert_equal 'FOO is to bar as BAZ is to...?', event.message 53 | assert_equal :incoming_msg, event.parent.type 54 | assert_equal "\001FOO is to bar as BAZ is to...?\001", event.parent.message 55 | assert_nil event.parent.parent 56 | 57 | # Action to channel 58 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com PRIVMSG #bottest :\001ACTION gives Towelie a joint\001") 59 | assert_equal 'Nerdmaster', event.nick 60 | assert_equal :incoming_act, event.type 61 | assert_equal '#bottest', event.channel 62 | assert_equal false, event.pm? 63 | assert_equal 'gives Towelie a joint', event.message 64 | assert_equal :incoming_ctcp, event.parent.type 65 | assert_equal "ACTION gives Towelie a joint", event.parent.message 66 | assert_equal :incoming_msg, event.parent.parent.type 67 | assert_equal "\001ACTION gives Towelie a joint\001", event.parent.parent.message 68 | assert_nil event.parent.parent.parent 69 | 70 | # PM to channel with less common prefix 71 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com PRIVMSG !bottest :foo") 72 | assert_equal :incoming_msg, event.type 73 | assert_equal false, event.pm? 74 | assert_equal '!bottest', event.channel 75 | end 76 | 77 | # Quick test of a numeric message I've ACTUALLY SEEN!! 78 | def test_numeric 79 | event = Net::YAIL::IncomingEvent.parse(':nerdbucket.com 266 Nerdmaster :Current global users: 22 Max: 33') 80 | assert_equal 'Current global users: 22 Max: 33', event.message 81 | assert_equal :incoming_numeric_266, event.type 82 | assert_equal 'nerdbucket.com', event.servername 83 | assert_equal 'nerdbucket.com', event.from 84 | assert_equal 'Nerdmaster', event.target 85 | 86 | assert !event.respond_to?(:nick) 87 | assert !event.respond_to?(:channel) 88 | assert !event.respond_to?(:fullname) 89 | assert event.server? 90 | 91 | assert_equal :incoming_numeric, event.parent.type 92 | assert_equal 266, event.parent.numeric 93 | 94 | # Numeric with multiple args 95 | event = Net::YAIL::IncomingEvent.parse(':someserver.co.uk.fn.bb 366 Towelie #bottest :End of /NAMES list.') 96 | assert_equal :incoming_numeric_366, event.type 97 | assert_equal '#bottest End of /NAMES list.', event.message 98 | assert_equal ['#bottest', 'End of /NAMES list.'], event.parameters 99 | 100 | # First param in the message params list should still be nick 101 | assert_equal 'Towelie', event.msg.params.first 102 | 103 | # This is a weird numeric causing errors for a user 104 | event = Net::YAIL::IncomingEvent.parse(":irc.somehost.xx 322 user #CrAzY_MaNiCoMiCuM 12 :[+ntr] \253'\002\0033,3 \0030 I \0030,0 \0034T \0034,4 " + 105 | "\0031A \0031,1\0038\273\0031,9 OrA NuOvO ErOtIcI Su FFTXL\0030,13 #CrAzY_MaNiCoMiCuM:\0032,4 QuI ReGnAnO PeR OrA I + PaZZi DeL WeB, " + 106 | "\0031,8TuTTi AnCoRa In PRoVa..\0034M\00307w\00308a\00303H\00311u\0031 \0031,9PrEpArAtE Le RiChIeStE PeR Il RiCoVeRo a ViTa\0030,13PoStI " + 107 | "LiBeRi!!\0031,4!PaZZi Da STaRe InSiEmE. :\336") 108 | assert_equal 'irc.somehost.xx', event.from 109 | assert_equal :incoming_numeric_322, event.type 110 | assert_equal :incoming_numeric, event.parent.type 111 | assert_equal 'user', event.target 112 | assert_equal ['#CrAzY_MaNiCoMiCuM', '12'], event.parameters[0..1] 113 | assert event.server? 114 | assert_equal "#CrAzY_MaNiCoMiCuM 12 [+ntr] \253'\002\0033,3 \0030 I \0030,0 \0034T \0034,4 \0031A \0031,1\0038\273\0031,9 OrA NuOvO " + 115 | "ErOtIcI Su FFTXL\0030,13 #CrAzY_MaNiCoMiCuM:\0032,4 QuI ReGnAnO PeR OrA I + PaZZi DeL WeB, \0031,8TuTTi AnCoRa In PRoVa..\0034M" + 116 | "\00307w\00308a\00303H\00311u\0031 \0031,9PrEpArAtE Le RiChIeStE PeR Il RiCoVeRo a ViTa\0030,13PoStI LiBeRi!!\0031,4!PaZZi Da " + 117 | "STaRe InSiEmE. :\336", event.message 118 | end 119 | 120 | # Test an invite 121 | def test_invite 122 | event = Net::YAIL::IncomingEvent.parse(':Nerdmaster!jeremy@nerdbucket.com INVITE Nerdminion :#nerd-talk') 123 | assert_equal '#nerd-talk', event.channel 124 | assert_equal 'Nerdmaster', event.nick 125 | assert_equal 'Nerdminion', event.target 126 | end 127 | 128 | # Test a user joining message 129 | def test_join 130 | event = Net::YAIL::IncomingEvent.parse(':Nerdminion!minion@nerdbucket.com JOIN :#nerd-talk') 131 | assert_equal '#nerd-talk', event.channel 132 | assert_equal 'Nerdminion', event.nick 133 | assert_equal :incoming_join, event.type 134 | end 135 | 136 | def test_part 137 | event = Net::YAIL::IncomingEvent.parse(':Nerdminion!minion@nerdbucket.com PART #nerd-talk :No, YOU GO TO HELL') 138 | assert_equal '#nerd-talk', event.channel 139 | assert_equal 'Nerdminion', event.nick 140 | assert_equal 'No, YOU GO TO HELL', event.message 141 | assert_equal :incoming_part, event.type 142 | end 143 | 144 | def test_kick 145 | event = Net::YAIL::IncomingEvent.parse(%q|:Nerdmaster!jeremy@nerdbucket.com KICK #nerd-talk Nerdminion :You can't quit! You're FIRED!|) 146 | assert_equal '#nerd-talk', event.channel 147 | assert_equal 'Nerdminion', event.target 148 | assert_equal 'Nerdmaster', event.nick 149 | assert_equal :incoming_kick, event.type 150 | assert_equal %q|You can't quit! You're FIRED!|, event.message 151 | end 152 | 153 | def test_quit 154 | event = Net::YAIL::IncomingEvent.parse(':TheTowel!ce611d7b0@nerdbucket.com QUIT :Bye bye') 155 | assert_equal 'TheTowel', event.nick 156 | assert_equal :incoming_quit, event.type 157 | assert_equal 'Bye bye', event.message 158 | end 159 | 160 | def test_nick 161 | # Nick change when nick is "unusual" - this also tests the bug with a single parameter being 162 | # treated incorrectly 163 | event = Net::YAIL::IncomingEvent.parse(':[|\|1]!~nerdmaste@nerd.nerdbucket.com NICK :Deadnerd') 164 | assert_equal '[|\|1]', event.nick 165 | assert_equal :incoming_nick, event.type 166 | assert_equal 'Deadnerd', event.message 167 | end 168 | 169 | # Test some notice stuff 170 | def test_notice_and_ctcp_reply 171 | event = Net::YAIL::IncomingEvent.parse(":nerdbucket.com NOTICE Nerdminion :You suck. A lot.") 172 | assert_equal 'nerdbucket.com', event.servername 173 | assert_equal 'nerdbucket.com', event.from 174 | assert event.server? 175 | assert_equal :incoming_notice, event.type 176 | assert_equal 'Nerdminion', event.target 177 | assert !event.respond_to?(:nick) 178 | assert !event.respond_to?(:fullname) 179 | assert_equal 'You suck. A lot.', event.message 180 | 181 | # This CTCP message... 182 | # ":Nerdmaster!jeremy@nerdbucket.com PRIVMSG Nerdminion \001USERINFO\001" 183 | # ...might yield this response: 184 | event = Net::YAIL::IncomingEvent.parse(":Nerdminion!minion@nerdbucket.com NOTICE Nerdmaster :\001USERINFO :Minion of the nerd\001") 185 | assert !event.respond_to?(:servername) 186 | assert_equal :incoming_ctcp_reply, event.type 187 | assert_equal 'Nerdmaster', event.target 188 | assert_equal 'Nerdminion', event.nick 189 | assert_equal 'Nerdminion!minion@nerdbucket.com', event.fullname 190 | assert_equal 'Nerdminion', event.from 191 | assert_equal 'USERINFO :Minion of the nerd', event.message 192 | 193 | # Channel-wide notice 194 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com NOTICE #channel-ten-news :Tonight's late-breaking story...") 195 | assert !event.respond_to?(:servername) 196 | assert_equal :incoming_notice, event.type 197 | assert_equal 'Nerdmaster', event.nick 198 | assert_equal '#channel-ten-news', event.channel 199 | assert_nil event.target 200 | assert_equal 'Nerdmaster!jeremy@nerdbucket.com', event.fullname 201 | assert_equal %q|Tonight's late-breaking story...|, event.message 202 | end 203 | 204 | def test_modes 205 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com MODE #bots +ob Towelie Doogles!*@*") 206 | assert !event.respond_to?(:servername) 207 | assert_equal 'Nerdmaster', event.nick 208 | assert_equal :incoming_mode, event.type 209 | assert_equal '#bots', event.channel 210 | assert_equal ['Towelie', 'Doogles!*@*'], event.targets 211 | assert_equal '+ob', event.message 212 | 213 | # Newly-created channels do this 214 | event = Net::YAIL::IncomingEvent.parse(':nerdbucket.com MODE #bots +nt') 215 | assert event.server? 216 | assert_equal 'nerdbucket.com', event.servername 217 | 218 | # TODO: Parse modes better! This case will be interesting, as the "i" is channel-specific. Useful 219 | # parsing would give us something like {'#bots' => '-i', 'Doogles!*@*' => '-b', 'Towelie' => '-v', 'Nerdmaster' => '-v'} 220 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com MODE #bots -bivv Doogles!*@* Towelie Nerdmaster") 221 | assert_equal 'Nerdmaster', event.nick 222 | assert_equal :incoming_mode, event.type 223 | assert_equal '#bots', event.channel 224 | assert_equal ['Doogles!*@*', 'Towelie', 'Nerdmaster'], event.targets 225 | assert_equal '-bivv', event.message 226 | 227 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com MODE #bots +m") 228 | assert_equal 'Nerdmaster', event.nick 229 | assert_equal :incoming_mode, event.type 230 | assert_equal '#bots', event.channel 231 | assert_equal [], event.targets 232 | assert_equal '+m', event.message 233 | 234 | # TODO: This is even worse than above - this is a pretty specific message (setting channel key 235 | # to 'foo'), but has to be parsed in a pretty absurd way to get that info. 236 | event = Net::YAIL::IncomingEvent.parse(":Nerdmaster!jeremy@nerdbucket.com MODE #bots +k foo") 237 | assert_equal 'Nerdmaster', event.nick 238 | assert_equal :incoming_mode, event.type 239 | assert_equal '#bots', event.channel 240 | assert_equal ['foo'], event.targets 241 | assert_equal '+k', event.message 242 | end 243 | 244 | # Simple test of error event 245 | def test_error 246 | event = Net::YAIL::IncomingEvent.parse 'ERROR :Closing Link: nerdbucket.com (Quit: Terminated by user)' 247 | assert_equal :incoming_error, event.type 248 | assert_equal 'Closing Link: nerdbucket.com (Quit: Terminated by user)', event.message 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /tests/tc_yail.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + "/net_yail" 3 | require File.dirname(__FILE__) + "/mock_irc" 4 | require "test/unit" 5 | 6 | # This test suite is built as an attempt to validate basic functionality in YAIL. Due to the 7 | # threading of the library, things are going to be... weird. Good luck, me. 8 | class YailSessionTest < Test::Unit::TestCase 9 | def setup 10 | @mockirc = MockIRC.new 11 | @log = Logger.new($stderr) 12 | @log.level = Logger::WARN 13 | @yail = Net::YAIL.new( 14 | :io => @mockirc, :address => "fake-irc.nerdbucket.com", :log => @log, 15 | :nicknames => ["Bot"], :realname => "Net::YAIL", :username => "Username" 16 | ) 17 | end 18 | 19 | # Sets up all our handlers the legacy way - allows testing that things work as they used to 20 | def setup_legacy_handling 21 | ### 22 | # Simple counters for basic testing of successful handler registration - note that all handlers 23 | # must add "; false" to the end to avoid stopping the built-in handlers 24 | ### 25 | 26 | @msg = Hash.new(0) 27 | @yail.prepend_handler(:incoming_welcome) { |message, args| @msg[:welcome] += 1; false } 28 | @yail.prepend_handler(:incoming_endofmotd) { |message, args| @msg[:endofmotd] += 1; false } 29 | @yail.prepend_handler(:incoming_notice) { |f, actor, target, message| @msg[:notice] += 1; false } 30 | @yail.prepend_handler(:incoming_nick) { |f, actor, nick| @msg[:nick] += 1; false } 31 | @yail.prepend_handler(:incoming_bannedfromchan) { |message, args| @msg[:bannedfromchan] += 1; false } 32 | @yail.prepend_handler(:incoming_join) { |f, actor, target| @msg[:join] += 1; false } 33 | @yail.prepend_handler(:incoming_mode) { |f, actor, target, modes, objects| @msg[:mode] += 1; false } 34 | @yail.prepend_handler(:incoming_msg) { |f, actor, target, message| @msg[:msg] += 1; false } 35 | @yail.prepend_handler(:incoming_act) { |f, actor, target, message| @msg[:act] += 1; false } 36 | @yail.prepend_handler(:incoming_ctcp) { |f, actor, target, message| @msg[:ctcp] += 1; false } 37 | @yail.prepend_handler(:incoming_ping) { |message| @msg[:ping] += 1; false } 38 | @yail.prepend_handler(:incoming_quit) { |f, actor, message| @msg[:quit] += 1; false } 39 | @yail.prepend_handler(:outgoing_mode) { |target, modes, objects| @msg[:o_mode] += 1; false } 40 | @yail.prepend_handler(:outgoing_join) { |channel, pass| @msg[:o_join] += 1; false } 41 | 42 | ### 43 | # More complex handlers to test parsing of messages 44 | ### 45 | 46 | # Channels list helps us test joins 47 | @channels = [] 48 | @yail.prepend_handler(:incoming_join) do |fullactor, actor, target| 49 | @channels.push(target) if @yail.me == actor 50 | end 51 | 52 | # Gotta store extra info on notices to test event parsing 53 | @notices = [] 54 | @yail.prepend_handler(:incoming_notice) do |f, actor, target, message| 55 | @notices.push({:from => f, :nick => actor, :target => target, :message => message}) 56 | end 57 | 58 | @yail.prepend_handler(:incoming_ping) { |message| @ping_message = message; false } 59 | @yail.prepend_handler(:incoming_quit) { |f, actor, message| @quit = {:full => f, :nick => actor, :message => message}; false } 60 | @yail.prepend_handler(:outgoing_join) { |channel, pass| @out_join = {:channel => channel, :password => pass}; false } 61 | @yail.prepend_handler(:outgoing_kick) { |nick, channel, reason| @out_kick = {:channel => channel, :nick => nick, :message => reason}; false } 62 | @yail.prepend_handler(:incoming_msg) { |f, actor, channel, message| @privmsg = {:channel => channel, :nick => actor, :message => message}; false } 63 | @yail.prepend_handler(:incoming_ctcp) { |f, actor, channel, message| @ctcp = {:channel => channel, :nick => actor, :message => message}; false } 64 | @yail.prepend_handler(:incoming_act) { |f, actor, channel, message| @act = {:channel => channel, :nick => actor, :message => message}; false } 65 | end 66 | 67 | # "New" handlers are set up (the 1.5+ way of doing things) here to perform tests in common with 68 | # legacy. Note that because handlers are different, we have to use filtering for things like the 69 | # welcome message, otherwise we don't let YAIL do its default stuff. 70 | def setup_new_handlers 71 | ### 72 | # Simple counters for basic testing of successful handler registration 73 | ### 74 | 75 | @msg = Hash.new(0) 76 | @yail.heard_welcome { @msg[:welcome] += 1 } 77 | @yail.heard_endofmotd { @msg[:endofmotd] += 1 } 78 | @yail.heard_notice { @msg[:notice] += 1 } 79 | @yail.heard_nick { @msg[:nick] += 1 } 80 | @yail.heard_bannedfromchan { @msg[:bannedfromchan] += 1 } 81 | @yail.heard_join { @msg[:join] += 1 } 82 | @yail.heard_mode { @msg[:mode] += 1 } 83 | @yail.heard_msg { @msg[:msg] += 1 } 84 | @yail.heard_act { @msg[:act] += 1 } 85 | @yail.heard_ctcp { @msg[:ctcp] += 1 } 86 | @yail.heard_ping { @msg[:ping] += 1 } 87 | @yail.heard_quit { @msg[:quit] += 1 } 88 | @yail.said_mode { @msg[:o_mode] += 1 } 89 | @yail.said_join { @msg[:o_join] += 1 } 90 | 91 | ### 92 | # More complex handlers to test parsing of messages 93 | ### 94 | 95 | # Channels list helps us test joins 96 | @channels = [] 97 | @yail.on_join do |event| 98 | @channels.push(event.channel) if @yail.me == event.nick 99 | end 100 | 101 | # Gotta store extra info on notices to test event parsing 102 | @notices = [] 103 | @yail.on_notice do |event| 104 | # Notices are tricky - we have to check server? and pm? to mimic legacy handler info 105 | notice = {:from => event.from, :message => event.message} 106 | notice[:nick] = event.server? ? "" : event.nick 107 | notice[:target] = event.pm? ? event.target : event.channel 108 | @notices.push notice 109 | end 110 | 111 | @yail.heard_ping { |event| @ping_message = event.message } 112 | @yail.on_quit { |event| @quit = {:full => event.fullname, :nick => event.nick, :message => event.message} } 113 | @yail.saying_join { |event| @out_join = {:channel => event.channel, :password => event.password} } 114 | @yail.saying_kick { |event| @out_kick = {:channel => event.channel, :nick => event.nick, :message => event.message} } 115 | @yail.on_msg { |event| @privmsg = {:channel => event.channel, :nick => event.nick, :message => event.message} } 116 | @yail.on_ctcp { |event| @ctcp = {:channel => event.channel, :nick => event.nick, :message => event.message} } 117 | @yail.on_act { |event| @act = {:channel => event.channel, :nick => event.nick, :message => event.message} } 118 | end 119 | 120 | # Waits until the mock IRC reports it has no more output - i.e., we've read everything available 121 | def wait_for_irc 122 | while @mockirc.ready? 123 | sleep 0.05 124 | end 125 | 126 | # For safety, we need to wait yet again to be sure YAIL has processed the data it read. 127 | # This is hacky, but it decreases random failures quite a bit 128 | sleep 0.1 129 | end 130 | 131 | # Log in to fake server, do stuff, see that basic handling and such are working. For simplicity, 132 | # this will be the all-encompassing "everything" test for legacy handling 133 | def test_legacy 134 | # Set up legacy handlers 135 | setup_legacy_handling 136 | 137 | common_tests 138 | end 139 | 140 | # Exact same tests as above - just verifying functionality is the same as it was in legacy 141 | def test_new 142 | setup_new_handlers 143 | wait_for_irc 144 | 145 | common_tests 146 | end 147 | 148 | # Resets the messages hash, mocks the IRC server to send string to us, waits for the response, yields to the block 149 | def mock_message(string) 150 | @msg = Hash.new(0) 151 | @mockirc.add_output string 152 | wait_for_irc 153 | yield 154 | end 155 | 156 | # Runs basic tests, verifying that we get expected results from a mocked session. Handlers set 157 | # via legacy prepend_handler should be just the same as new handler system. 158 | def common_tests 159 | @yail.start_listening 160 | 161 | # Wait until all data has been read and check messages 162 | wait_for_irc 163 | assert_equal 1, @msg[:welcome] 164 | assert_equal 1, @msg[:endofmotd] 165 | assert_equal 3, @msg[:notice] 166 | 167 | # Intense notice test - make sure all events were properly translated 168 | assert_equal ['fakeirc.org', nil, 'fakeirc.org'], @notices.collect {|n| n[:from]} 169 | assert_equal ['', '', ''], @notices.collect {|n| n[:nick]} 170 | assert_equal ['AUTH', 'AUTH', 'Bot'], @notices.collect {|n| n[:target]} 171 | assert_match %r|looking up your host|i, @notices.first[:message] 172 | assert_match %r|looking up your host|i, @notices[1][:message] 173 | assert_match %r|you are exempt|i, @notices.last[:message] 174 | 175 | # Test magic methods that set up the bot 176 | assert_equal "Bot", @yail.me, "Should have set @yail.me automatically on welcome handler" 177 | assert_equal 1, @msg[:o_mode], "Should have auto-sent mode +i" 178 | 179 | # Make sure nick change works 180 | @yail.nick "Foo" 181 | wait_for_irc 182 | assert_equal "Foo", @yail.me, "Should have set @yail.me on explicit nick change" 183 | 184 | # Join a channel where we've been banned 185 | @yail.join("#banned") 186 | wait_for_irc 187 | assert_equal 1, @msg[:bannedfromchan] 188 | assert_equal "#banned", @out_join[:channel] 189 | assert_equal "", @out_join[:password] 190 | assert_equal [], @channels 191 | 192 | # Join some other channel 193 | @yail.join("#foosball", "pass") 194 | wait_for_irc 195 | assert_equal "#foosball", @out_join[:channel] 196 | assert_equal "pass", @out_join[:password] 197 | assert_equal ['#foosball'], @channels 198 | 199 | # Kick somebody 200 | @yail.kick("Nerdminion", "#foosball") 201 | wait_for_irc 202 | assert_equal("#foosball", @out_kick[:channel]) 203 | assert_equal("Nerdminion", @out_kick[:nick]) 204 | 205 | # Kick with message 206 | @yail.kick("Nerdminion", "#foosball", "Because you're a bad, bad man") 207 | wait_for_irc 208 | assert_equal("#foosball", @out_kick[:channel]) 209 | assert_equal("Nerdminion", @out_kick[:nick]) 210 | assert_equal("Because you're a bad, bad man", @out_kick[:message]) 211 | 212 | # Mock some chatter to verify PRIVMSG info 213 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :#{@yail.me}: Welcome!" do 214 | assert_equal 1, @msg[:msg] 215 | assert_equal 0, @msg[:act] 216 | assert_equal 0, @msg[:ctcp] 217 | 218 | assert_equal "Nerdmaster", @privmsg[:nick] 219 | assert_equal "#foosball", @privmsg[:channel] 220 | assert_equal "#{@yail.me}: Welcome!", @privmsg[:message] 221 | end 222 | 223 | # CTCP 224 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :\001CTCP THING\001" do 225 | assert_equal 0, @msg[:msg] 226 | assert_equal 0, @msg[:act] 227 | assert_equal 1, @msg[:ctcp] 228 | 229 | assert_equal "Nerdmaster", @ctcp[:nick] 230 | assert_equal "#foosball", @ctcp[:channel] 231 | assert_equal "CTCP THING", @ctcp[:message] 232 | end 233 | 234 | # ACT 235 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :\001ACTION vomits on you\001" do 236 | assert_equal 0, @msg[:msg] 237 | assert_equal 1, @msg[:act] 238 | assert_equal 0, @msg[:ctcp] 239 | 240 | assert_equal "Nerdmaster", @act[:nick] 241 | assert_equal "#foosball", @act[:channel] 242 | assert_equal "vomits on you", @act[:message] 243 | end 244 | 245 | # PING 246 | mock_message "PING boo" do 247 | assert_equal 1, @msg[:ping] 248 | assert_equal 'boo', @ping_message 249 | end 250 | 251 | # User quits 252 | mock_message ":Nerdmaster!nerd@nerdbucket.com QUIT :Quit: Bye byes" do 253 | assert_equal 1, @msg[:quit] 254 | assert_equal 'Nerdmaster!nerd@nerdbucket.com', @quit[:full] 255 | assert_equal 'Nerdmaster', @quit[:nick] 256 | assert_equal 'Quit: Bye byes', @quit[:message] 257 | end 258 | end 259 | 260 | # Verifies that chains are broken properly for before filters and the callback, but not for after filters 261 | def test_chain_breaking 262 | @msg = Hash.new(0) 263 | 264 | # First call means last filter - this one proves that the last one did or didn't run 265 | @yail.hearing_msg { |e| @msg[:before_last] += 1; e.handled! if e.message == "quit 2" } 266 | 267 | # Second call means first filter - this one actually allows or skips all other filters and callbacks 268 | @yail.hearing_msg { |e| e.handled! if e.message == "quit 1" } 269 | 270 | # Actual callback - this is hit as long as a before filter didn't stop the chain 271 | @yail.on_msg { |e| @msg[:callback] += 1; e.handled! } 272 | 273 | # After filters never stop the chain 274 | @yail.heard_msg { |e| @msg[:after_msg] += 1; e.handled! } 275 | @yail.heard_msg { |e| @msg[:after_msg] += 1; e.handled! } 276 | @yail.heard_msg { |e| @msg[:after_msg] += 1; e.handled! } 277 | @yail.heard_msg { |e| @msg[:after_msg] += 1; e.handled! } 278 | 279 | @yail.start_listening 280 | 281 | wait_for_irc 282 | 283 | # quit 1 means no filters are hit after the first 284 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :quit 1" do 285 | assert_equal 0, @msg[:before_last] 286 | assert_equal 0, @msg[:callback] 287 | assert_equal 0, @msg[:after_msg] 288 | end 289 | 290 | # quit 2 means we hit the first before filter, but stopped after the second 291 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :quit 2" do 292 | assert_equal 1, @msg[:before_last] 293 | assert_equal 0, @msg[:callback] 294 | assert_equal 0, @msg[:after_msg] 295 | end 296 | 297 | # After filters get run when before filters don't stop things, even though callback and after 298 | # filters keep trying to break the chain 299 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :Well hello there, test!" do 300 | assert_equal 1, @msg[:before_last] 301 | assert_equal 1, @msg[:callback] 302 | assert_equal 4, @msg[:after_msg] 303 | end 304 | end 305 | 306 | # Verify that *_any filters are run appropriately and in order - before filter should be the 307 | # first filter run, and after filter should be the last 308 | def test_any_filters_and_filter_order 309 | step = 1 310 | 311 | # First we should hit the incoming "any" before filter 312 | @yail.hearing_any do |e| 313 | if e.type == :incoming_msg 314 | assert_equal 1, step 315 | step += 1 316 | end 317 | end 318 | 319 | # Second, incoming message before filter 320 | @yail.hearing_msg { |e| assert_equal 2, step; step += 1 } 321 | 322 | # Third, the callback 323 | @yail.on_msg { |e| assert_equal 3, step; step += 1 } 324 | 325 | # Fourth, incoming message after callback 326 | @yail.heard_msg { |e| assert_equal 4, step; step += 1 } 327 | 328 | # Last, incoming "any" after filter 329 | @yail.heard_any do |e| 330 | if e.type == :incoming_msg 331 | assert_equal 5, step 332 | end 333 | end 334 | 335 | @yail.start_listening 336 | 337 | wait_for_irc 338 | 339 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :Well hello there, test!" do 340 | # We should have no handlers hit more than once - step should be 5 still 341 | assert_equal 5, step 342 | end 343 | end 344 | 345 | def test_conditional_filters 346 | @msg = Hash.new(0) 347 | 348 | # Conditional filters are really ugly when passing a block instead of a method, so let's 349 | # at least try to make this look decent 350 | hearing_food = lambda { |e| @msg[:food] += 1 } 351 | not_hearing_bad = lambda { |e| @msg[:not_bad] += 1 } 352 | heard_nothing = lambda { |e| @msg[:nothing] += 1 } 353 | 354 | # If the message looks like "food", the handler will be hit 355 | @yail.hearing_msg(hearing_food, :if => lambda {|e| e.message =~ /food/}) 356 | 357 | # Unless the message looks like "bad", the handler will be hit 358 | @yail.hearing_msg(not_hearing_bad, :unless => lambda {|e| e.message =~ /bad/}) 359 | 360 | # Verify after-filter is called, and hash conditions are respected 361 | @yail.heard_msg(heard_nothing, :if => {:message => ""}) 362 | 363 | # Verify multiple hash conditions are anded - do it with a block just so we can see how 364 | # wonderfully bad it looks 365 | @yail.heard_msg(:if => {:message => "bah", :pm? => true}) do 366 | @msg[:heard_2] += 1 367 | end 368 | 369 | # GO GO GO 370 | @yail.start_listening 371 | 372 | wait_for_irc 373 | 374 | # Food 375 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :this food is bad" do 376 | assert_equal({:food => 1}, @msg) 377 | end 378 | 379 | # Good food! 380 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :this food is good!" do 381 | assert_equal({:food => 1, :not_bad => 1}, @msg) 382 | end 383 | 384 | # Good drink 385 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :this drink is good!" do 386 | assert_equal({:not_bad => 1}, @msg) 387 | end 388 | 389 | # Nothing, but not bad 390 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :" do 391 | assert_equal({:not_bad => 1, :nothing => 1}, @msg) 392 | end 393 | 394 | # Private message for multiple hash test 395 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #{@yail.me} :bah" do 396 | assert_equal({:not_bad => 1, :heard_2 => 1}, @msg) 397 | end 398 | 399 | # Private message without bah does *not* fire off multiple hash handler 400 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #{@yail.me} :hey there buddy" do 401 | assert_equal({:not_bad => 1}, @msg) 402 | end 403 | 404 | # "bah" without being pm doesn't fire off handler, either 405 | mock_message ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :bah" do 406 | assert_equal({:not_bad => 1}, @msg) 407 | end 408 | end 409 | 410 | def test_split_messages 411 | # We have to set up a whole new YAIL for this since split messages only work on SSL 412 | @yail = Net::YAIL.new( 413 | :io => @mockirc, :address => "fake-irc.nerdbucket.com", :log => @log, 414 | :nicknames => ["Bot"], :realname => "Net::YAIL", :username => "Username", 415 | :use_ssl => true 416 | ) 417 | 418 | @privmsg = [] 419 | @yail.on_msg { |event| @privmsg.push({:channel => event.channel, :nick => event.nick, :message => event.message}) } 420 | @mockirc.add_partial_output ":Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :First line\n" 421 | @mockirc.add_partial_output ":Nerdmaster!nerd@n" 422 | @mockirc.add_partial_output "erdbucket.com PRIVMSG" 423 | @mockirc.add_partial_output " #foosball :Second line\n:Nerdmaster!nerd@nerdbucket.com PRIVMSG #foosball :Third line\n" 424 | @yail.start_listening 425 | wait_for_irc 426 | 427 | assert_equal(@privmsg.length, 3) 428 | 429 | 0.upto(2) do |idx| 430 | assert_equal("Nerdmaster", @privmsg[idx][:nick]) 431 | assert_equal("#foosball", @privmsg[idx][:channel]) 432 | end 433 | 434 | assert_equal("First line", @privmsg[0][:message]) 435 | assert_equal("Second line", @privmsg[1][:message]) 436 | assert_equal("Third line", @privmsg[2][:message]) 437 | end 438 | end 439 | -------------------------------------------------------------------------------- /lib/net/yail.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'thread' 3 | require 'yaml' 4 | require 'logger' 5 | require 'openssl' 6 | 7 | # To make this library seem smaller, a lot of code has been split up and put 8 | # into semi-logical files. I don't really like this hacky solution, but I 9 | # cannot figure out a nicer way to keep the code as clean as I like. 10 | require 'net/yail/magic_events' 11 | require 'net/yail/default_events' 12 | require 'net/yail/output_api' 13 | require 'net/yail/legacy_events' 14 | require 'net/yail/dispatch' 15 | 16 | # This tells us our version info. 17 | require 'net/yail/yail-version' 18 | 19 | # Finally, real classes to include! 20 | require 'net/yail/event' 21 | require 'net/yail/handler' 22 | 23 | # If a thread crashes, I want the app to die. My threads are persistent, not 24 | # temporary. 25 | Thread.abort_on_exception = true 26 | 27 | module Net 28 | 29 | # This library is based on the initial release of IRCSocket with a tiny bit 30 | # of plagarism of Ruby-IRC. 31 | # 32 | # Need an example? For a separate project you can play with that relies on Net::YAIL, check out 33 | # https://github.com/Nerdmaster/superloud. This is based on the code in the examples directory, 34 | # but is easier to clone, run, and tinker with because it's a separate github project. 35 | # 36 | # My aim here is to build something that is still fairly simple to use, but 37 | # powerful enough to build a decent IRC program. 38 | # 39 | # This is far from complete, but it does successfully power a relatively 40 | # complicated bot, so I believe it's solid and "good enough" for basic tasks. 41 | # 42 | # =Events overview 43 | # 44 | # YAIL at its core is an event handler with some logic specific to IRC socket messages. BaseEvent 45 | # is the parent of all event objects. An event is run through various pre-callback filters, a 46 | # single callback, and post-callback filters. Up until the callback is hit, the handler 47 | # "chain" can be stopped by calling the event's .handled! method. It is generally advised against 48 | # doing this, as it will stop things like post-callback stats gathering and similar plugin-friendly 49 | # features, but it does make sense in certain situations (an "ignore user" module, for instance). 50 | # 51 | # The life of a typical event, such as the one generated when a server message is parsed into a Net::YAIL::IncomingEvent object: 52 | # 53 | # * If the event hasn't been handled, the event's callback is run 54 | # * If the event hasn't been handled, legacy handlers are run if any are registered (TO BE REMOVED IN 2.0) 55 | # * Legacy handlers can return true to end the chain, much like calling BaseEvent#handle! on an event object 56 | # * If the event hasn't been handled, all "after filters" are run (these cannot set an event as having been handled) 57 | # 58 | # ==Callbacks and Filters 59 | # 60 | # Callbacks and filters are basically handlers for a given event. The difference in a callback 61 | # and filter is explained above (1 callback per event, many filters), but at their core they are 62 | # just code that handles some aspect of the event. 63 | # 64 | # Handler methods must receive a block of code. This can be passed in as a simple Ruby block, or 65 | # manually created via Proc.new, lambda, Foo.method(:bar), etc. The method parameter of 66 | # all the handler methods is optional so that, as mentioned, a block can be used instead of a Proc. 67 | # 68 | # The handlers, when fired, will yield the event object containing all relevant data for the event. 69 | # See the examples below for a basic idea. 70 | # 71 | # To register an event's callback, you have the following options: 72 | # * set_callback(event_type, method = nil, &block): Sets the event type's callback, clobbering any 73 | # existing callback for that event type. 74 | # * on_xxx(method = nil, &block): For incoming events only, this is a shortcut for set_callback. 75 | # The "xxx" must be replaced by the incoming event's short type name. For example, 76 | # on_welcome {|event| ...} would be used in place of set_callback(:incoming_welcome, xxx). 77 | # 78 | # To register a before- or after-callback filter, the following methods are available: 79 | # * before_filter(event_type, method = nil, &block): Sets a before-callback filter, adding it to 80 | # the current list of before-callback filters for the given event type. 81 | # * after_filter(event_type, method = nil, &block): Sets an after-callback filter, adding it to 82 | # the current list of after-callback filters for the given event type. 83 | # * hearing_xxx(method = nil, &block): Adds a before-callback filter for the given incoming event 84 | # type, such as hearing_msg {|event| ...} 85 | # * heard_xxx(method = nil, &block): Adds an after-callback filter for the given incoming event 86 | # type, such as heard_msg {|event| ...} 87 | # * saying_xxx(method = nil, &block): Adds a before-callback filter for the given outgoing event 88 | # type, such as saying_mode {|event| ...} 89 | # * said_xxx(method = nil, &block): Adds an after-callback filter for the given outgoing event 90 | # type, such as said_act {|event| ...} 91 | # 92 | # ===Conditional Filtering 93 | # 94 | # For some situations, you want your filter to only be called if a certain condition is met. Enter conditional filtering! 95 | # By using this exciting feature, you can set up handlers and callbacks which only trigger when certain conditions are 96 | # met. Be warned, though, this can get confusing.... 97 | # 98 | # Conditions can be added to any filter method, but should **never** be used on the callback, since *there can be only one*. 99 | # To add a filter, you simply supply a hash with a key of either `:if` or `:unless`, and a value which is either another 100 | # hash of conditions, or a proc. 101 | # 102 | # If a proc is sent, it will be a method that is called and passed the event object. If the proc returns true, an `:if` 103 | # condition is met and un `:unless` condition is not met. If a condition is not met, the filter is skipped entirely. 104 | # 105 | # If a hash is sent, each key is expected to be an attribute on the event object. It's similar to a lambda where you 106 | # return true if each attribute equals the value in the hash. For instance, `:if => {:message => "food", :nick => "Simon"}` 107 | # is the same as `:if => lambda {|e| e.message == "food" && e.nick == "Simon"}`. 108 | # 109 | # ==Incoming events 110 | # 111 | # *All* incoming events will have, at the least, the following methods: 112 | # * raw: The raw text sent by the IRC server 113 | # * msg: The parsed IRC message (Net::YAIL::MessageParser instance) 114 | # * server?: Boolean flag. True if the message was generated by the server alone, false if it 115 | # was generated by some kind of user action (such as a PRIVMSG sent from somebody else) 116 | # * from: Originator of message: user's nickname if a user message, server name otherwise 117 | # 118 | # Additionally, *all messages originated by another IRC user* will have these methods: 119 | # * fullname: The full username ("Nerdmaster!jeremy@nerdbucket.com", for instance) 120 | # * nick: The short nickname of a user ("Nerdmaster", for instance) - this will be the 121 | # same as event.from, but obviously only for user-initiated events. 122 | # 123 | # Messages sent by the server that weren't initiated by a user will have event.servername, 124 | # which is merely the name of the server, and will be the same as event.from. 125 | # 126 | # When in doubt, you can always build a filter for a particular event that spits out all its 127 | # non-base methods: 128 | # yail.hearing_xxx {|e| puts e.public_methods - Net::YAIL::BaseEvent.instance_methods} 129 | # 130 | # This should be a comprehensive list of all incoming events and what additional attributes the 131 | # object will expose. 132 | # 133 | # * :incoming_any: A catch-all handler useful for reporting or doing top-level filtering. 134 | # Before- and after-callback filters can run for all events by adding them to :incoming_any, but 135 | # you cannot register a callback, as the event's type determines its callback. :incoming_any 136 | # before-callback filters can stop an event from happening on a global scale, so be careful when 137 | # deciding to do anything "clever" here. 138 | # * :incoming_error: A server error of some kind happened. event.message gives you the message sent 139 | # by the server. 140 | # * :incoming_ping: PING from server. YAIL handles this by default, so if you override the 141 | # handler, you MUST send a PONG response or the server will close your connection. event.message 142 | # may have a PING "message" in it. The return PONG should send out the same message as the PING 143 | # received. 144 | # * :incoming_topic_change: The topic of a channel was changed. event.channel gives you the 145 | # channel in which the change occurred, while event.message gives you the message, i.e. the new topic. 146 | # * :incoming_numeric_###: If you want, you can set up your handlers for numeric events by number, 147 | # but you'll have a much easier time looking at the eventmap.yml file included in the lib/net/yail 148 | # directory. You can create an incoming handler for any event in that file. The event names will 149 | # be :incoming_xxx, where "xxx" is the text of the event. For instance, you could use 150 | # set_callback(:incoming_liststart) {|event| ...} to handle the 321 numeric message, or just 151 | # on_liststart {|event| ...}. Exposes event.target, event.parameters, 152 | # event.message, and event.numeric. You may have to experiment with different 153 | # numerics to see what this data actually means for a given event. 154 | # * :incoming_invite: INVITE message sent from a user to request your presence in another channel. 155 | # Exposes event.channel, the channel in question, and event.target, which should always be 156 | # your nickname. 157 | # * :incoming_join: A user joined a channel. event.channel tells you the channel. 158 | # * :incoming_part: A user left a channel. event.channel tells you the channel, and 159 | # event.message will contain a message if the user gave one. 160 | # * :incoming_kick: A user was kicked from a channel. event.channel tells you 161 | # the channel, event.target tells you the nickname of the kicked party, and 162 | # event.message will contain a message if the kicking party gave one. 163 | # * :incoming_quit: A user quit the server. event.message will have details, if the 164 | # user provided a quit message. 165 | # * :incoming_nick: A user changed nicknames. event.message will contain the new 166 | # nickname. 167 | # * :incoming_mode: A user or server can initiate this, and this is the most screwy event 168 | # in YAIL. This needs an overhaul and will hopefully change by 2.0, but for now I take the raw 169 | # mode strings, such as "+bivv" and put them in event.message. All arguments of the 170 | # mode strings get stored as individual records in the event.targets array. For modes 171 | # like "+ob", the first entry in targets will be the user given ops, and the second will be the 172 | # ban string. I hope to overhaul this prior to 2.0, so if you rely on mode parsing, be warned. 173 | # * :incoming_msg: A "standard" PRIVMSG event (i.e., not CTCP). event.message will 174 | # contain the message, obviously. If the message is to a channel, event.channel 175 | # will contain the channel name, event.target will be nil, and event.pm? will 176 | # be false. If the message is sent to a user (the client running Net::YAIL), 177 | # event.channel will be nil, event.target will have the user name, and 178 | # event.pm? will be true. 179 | # * :incoming_ctcp: The behavior of event.target, event.channel, and 180 | # event.pm? will remain the same as for :incoming_msg events. 181 | # event.message will contain the CTCP message. 182 | # * :incoming_act: The behavior of event.target, event.channel, and 183 | # event.pm? will remain the same as for :incoming_msg events. 184 | # event.message will contain the ACTION message. 185 | # * :incoming_notice: The behavior of event.target, event.channel, and 186 | # event.pm? will remain the same as for :incoming_msg events. 187 | # event.message will contain the NOTICE message. 188 | # * :incoming_ctcp_reply: The behavior of event.target, event.channel, 189 | # and event.pm? will remain the same as for :incoming_msg events. 190 | # event.message will contain the CTCP reply message. 191 | # * :incoming_unknown: This should NEVER happen, but just in case, it's there. Enjoy! 192 | # 193 | # ==Output API 194 | # 195 | # All output API calls create a Net::YAIL::OutgoingEvent object and dispatch that event. After 196 | # before-callback filters are processed, assuming the event wasn't handled, the callback will send 197 | # the message out to the IRC socket. If you choose to override the callback for outgoing events, 198 | # rather than using filters, you will have to print the data to the socket yourself. 199 | # 200 | # The parameters for the API calls will match what the outgoing event object exposes as attributes, 201 | # so if there were an API call for "foo(bar, baz)", it would generate an outgoing event of type 202 | # :outgoing_foo. The data you passed in as "bar" would be available via event.bar in a handler. 203 | # 204 | # There is also an :outgoing_any event type that can be used for global filtering much like the 205 | # :incoming_any filtering. 206 | # 207 | # The :outgoing_begin_connection event callback should never be overwritten. It exists so 208 | # you can add filters before or after the initial flurry of messages to the server (USER, PASS, and 209 | # NICK), but it is really an internal "helper" event. Overwriting it means you will need to write 210 | # your own code to log in to the server. 211 | # 212 | # This should be a comprehensive list of all outgoing methods and parameters: 213 | # 214 | # * msg(target, message): Send a PRIVMSG to the given target (channel or nickname) 215 | # * ctcp(target, message): Sends a PRIVMSG to the given target with its message wrapped in 216 | # ASCII character 1, signifying use of client-to-client protocol. 217 | # * act(target, message): Sends a PRIVMSG to the given target with its message wrapped in the 218 | # CTCP "action" syntax. A lot of IRC clients use "/me" to do this command. 219 | # * privmsg(target, message): Sends a raw, unbuffered PRIVMSG to the given target - primarily 220 | # useful for filtering, as msg, act, and ctcp all eventually call this handler. 221 | # * notice(target, message): Sends a notice message to the given target 222 | # * ctcpreply(target, message): Sends a notice message wrapped in ASCII 1 to signify a CTCP reply. 223 | # * mode(target, [modes, [objects]]): Sets or requests modes for the given target 224 | # (channel or user). The list of modes, if present, is applied to the target and objects if 225 | # present. Modes in YAIL need some work, but here are some basic examples: 226 | # * mode("#channel", "+b", "Nerdmaster!*@*"): bans anybody with the nickname 227 | # "Nerdmaster" from subsequently joining #channel. 228 | # * mode("#channel"): Requests a list of modes on #channel 229 | # * mode("#channel", "-k"): Removes the key for #channel 230 | # * join(channel, [password]): Joins the given channel with an optional password (channel key) 231 | # * part(channel, [message]): Leaves the given channel, with an optional message specified on part 232 | # * quit([message]): Leaves the server with an optional message. Note that some servers will 233 | # not display your quit message due to spam issues. 234 | # * nick(nick): Changes your nickname, and updates YAIL @me variable if successful 235 | # * user(username, hostname, servername, realname): Sets up your information upon joining 236 | # a server. YAIL should generally take care of this for you in the default :outgoing_begin_connection 237 | # callback. 238 | # * pass(password): Sends a server password, not to be confused with a channel key. 239 | # * oper(user, password): Authenticates a user as an IRC operator for the server. 240 | # * topic(channel, [new_topic]): With no new_topic, returns the topic for a given channel. 241 | # If new_topic is present, sets the topic instead. 242 | # * names([channel]): Gets a list of all users on the network or a specific channel if specified. 243 | # The channel parameter can actually contain a comma-separated list of channels if desired. 244 | # * list([channel, [server]]: Shows all channels on the server. channel can 245 | # contain a comma-separated list of channels, which will restrict the list to the given channels. 246 | # If server is present, the request is forwarded to the given server. 247 | # * invite(nick, channel): Invites a user to the given channel. 248 | # * kick(nick, channel, [message]): Kicks the given user from the given channel with an optional message 249 | # * whois(nick, [server]): Issues a WHOIS command for the given nickname with an optional server. 250 | # 251 | # =Simple Example 252 | # 253 | # You should grab the source from github (https://github.com/Nerdmaster/ruby-irc-yail) and look at 254 | # the examples directory for more interesting (but still simple) examples. But to get you started, 255 | # here's a really dumb, contrived example: 256 | # 257 | # require 'rubygems' 258 | # require 'net/yail' 259 | # 260 | # irc = Net::YAIL.new( 261 | # :address => 'irc.someplace.co.uk', 262 | # :username => 'Frakking Bot', 263 | # :realname => 'John Botfrakker', 264 | # :nicknames => ['bot1', 'bot2', 'bot3'] 265 | # ) 266 | # 267 | # # Automatically join #foo when the server welcomes us 268 | # irc.on_welcome {|event| irc.join("#foo") } 269 | # 270 | # # Store the last message and person who spoke - this is a filter as it doesn't need to be 271 | # # "the" definitive code run for the event 272 | # irc.hearing_msg {|event| @last_message = {:nick => event.nick, :message => event.message} } 273 | # 274 | # # Loops forever until CTRL+C 275 | # irc.start_listening! 276 | class YAIL 277 | include Net::IRCEvents::Magic 278 | include Net::IRCEvents::Defaults 279 | include Net::IRCOutputAPI 280 | include Net::IRCEvents::LegacyEvents 281 | include Dispatch 282 | 283 | attr_reader( 284 | :me, # Nickname on the IRC server 285 | :registered, # If true, we've been welcomed 286 | :nicknames, # Array of nicknames to try when logging on to server 287 | :dead_socket, # True if @socket.eof? or read/connect fail 288 | :socket # TCPSocket instance 289 | ) 290 | attr_accessor( 291 | :throttle_seconds, 292 | :log 293 | ) 294 | 295 | def silent 296 | @log.warn '[DEPRECATED] - Net::YAIL#silent is deprecated as of 1.4.1 - .log can be used instead' 297 | return @log_silent 298 | end 299 | def silent=(val) 300 | @log.warn '[DEPRECATED] - Net::YAIL#silent= is deprecated as of 1.4.1 - .log can be used instead' 301 | @log_silent = val 302 | end 303 | 304 | def loud 305 | @log.warn '[DEPRECATED] - Net::YAIL#loud is deprecated as of 1.4.1 - .log can be used instead' 306 | return @log_loud 307 | end 308 | def loud=(val) 309 | @log.warn '[DEPRECATED] - Net::YAIL#loud= is deprecated as of 1.4.1 - .log can be used instead' 310 | @log_loud = val 311 | end 312 | 313 | # Makes a new instance, obviously. 314 | # 315 | # Note: I haven't done this everywhere, but for the constructor, I felt 316 | # it needed to have hash-based args. It's just cleaner to me when you're 317 | # taking this many args. 318 | # 319 | # Options: 320 | # * :address: Name/IP of the IRC server 321 | # * :port: Port number, defaults to 6667 322 | # * :username: Username reported to server 323 | # * :realname: Real name reported to server 324 | # * :nicknames: Array of nicknames to cycle through 325 | # * :io: TCP replacement object to use, should already be connected and ready for sending 326 | # the "connect" data (:outgoing_begin_connection handler does this) 327 | # If this is passed, :address and :port are ignored. 328 | # * :silent: DEPRECATED - Sets Logger level to FATAL and silences most non-Logger 329 | # messages. 330 | # * :loud: DEPRECATED - Sets Logger level to DEBUG. Spits out too many messages for your own good, 331 | # and really is only useful when debugging YAIL. Defaults to false, thankfully. 332 | # * :throttle_seconds: Seconds between a cycle of privmsg sends. 333 | # Defaults to 1. One "cycle" is defined as sending one line of output to 334 | # *all* targets that have output buffered. 335 | # * :server_password: Very optional. If set, this is the password 336 | # sent out to the server before USER and NICK messages. 337 | # * :log: Optional, if set uses this logger instead of the default (Ruby's Logger). 338 | # If set, :loud and :silent options are ignored. 339 | # * :log_io: Optional, ignored if you specify your own :log - sends given object to 340 | # Logger's constructor. Must be filename or IO object. 341 | # * :use_ssl: Defaults to false. If true, attempts to use SSL for connection. 342 | def initialize(options = {}) 343 | @me = '' 344 | @nicknames = options[:nicknames] 345 | @registered = false 346 | @username = options[:username] 347 | @realname = options[:realname] 348 | @address = options[:address] 349 | @io = options[:io] 350 | @port = options[:port] || 6667 351 | @log_silent = options[:silent] || false 352 | @log_loud = options[:loud] || false 353 | @throttle_seconds = options[:throttle_seconds] || 1 354 | @password = options[:server_password] 355 | @ssl = options[:use_ssl] || false 356 | 357 | ############################################# 358 | # TODO: DEPRECATED!! 359 | # 360 | # TODO: Delete this! 361 | ############################################# 362 | @legacy_handlers = Hash.new 363 | 364 | # Shared resources for threads to try and coordinate.... I know very 365 | # little about thread safety, so this stuff may be a terrible disaster. 366 | # Please send me better approaches if you are less stupid than I. 367 | @input_buffer = [] 368 | @input_buffer_mutex = Mutex.new 369 | @privmsg_buffer = {} 370 | @privmsg_buffer_mutex = Mutex.new 371 | 372 | # Buffered output is allowed to go out right away. 373 | @next_message_time = Time.now 374 | 375 | # Setup callback/filter hashes 376 | @before_filters = Hash.new 377 | @after_filters = Hash.new 378 | @callback = Hash.new 379 | 380 | # Special handling to avoid mucking with Logger constants if we're using a different logger 381 | if options[:log] 382 | @log = options[:log] 383 | else 384 | @log = Logger.new(options[:log_io] || STDERR) 385 | @log.level = Logger::INFO 386 | 387 | if (options[:silent] || options[:loud]) 388 | @log.warn '[DEPRECATED] - passing :silent and :loud options to constructor are deprecated as of 1.4.1' 389 | end 390 | 391 | # Convert old-school options into logger stuff 392 | @log.level = Logger::DEBUG if @log_loud 393 | @log.level = Logger::FATAL if @log_silent 394 | end 395 | 396 | # Read in map of event numbers and names. Yes, I stole this event map 397 | # file from RubyIRC and made very minor changes.... They stole it from 398 | # somewhere else anyway, so it's okay. 399 | eventmap = "#{File.dirname(__FILE__)}/yail/eventmap.yml" 400 | @event_number_lookup = File.open(eventmap) { |file| YAML::load(file) }.invert 401 | 402 | if @io 403 | @socket = @io 404 | else 405 | prepare_tcp_socket 406 | end 407 | 408 | set_defaults 409 | end 410 | 411 | # Starts listening for input and builds the perma-threads that check for 412 | # input, output, and privmsg buffering. 413 | def start_listening 414 | # We don't want to spawn an extra listener 415 | return if Thread === @ioloop_thread 416 | 417 | # Don't listen if socket is dead 418 | return if @dead_socket 419 | 420 | # Exit a bit more gracefully than just crashing out - allow any :outgoing_quit filters to run, 421 | # and even give the server a second to clean up before we fry the connection 422 | # 423 | # TODO: This REALLY doesn't belong here! This is saying everybody who uses the lib wants 424 | # CTRL+C to end the app at the YAIL level. Not necessarily true outside bot-land. 425 | quithandler = lambda { quit('Terminated by user'); sleep 1; stop_listening; exit } 426 | trap("INT", quithandler) 427 | trap("TERM", quithandler) 428 | 429 | # Begin the listening thread 430 | @ioloop_thread = Thread.new {io_loop} 431 | @input_processor = Thread.new {process_input_loop} 432 | @privmsg_processor = Thread.new {process_privmsg_loop} 433 | 434 | # Let's begin the cycle by telling the server who we are. This should start a TERRIBLE CHAIN OF EVENTS!!! 435 | dispatch OutgoingEvent.new(:type => :begin_connection, :username => @username, :address => @address, :realname => @realname) 436 | end 437 | 438 | # This starts the connection, threading, etc. as start_listening, but *forces* the user into 439 | # and endless loop. Great for a simplistic bot, but probably not universally desired. 440 | def start_listening! 441 | start_listening 442 | while !@dead_socket 443 | # This is more for CPU savings than actually needing a delay - CPU spikes if we never sleep 444 | sleep 0.05 445 | end 446 | end 447 | 448 | # Kills and clears all threads. See note above about my lack of knowledge 449 | # regarding threads. Please help me if you know how to make this system 450 | # better. DEAR LORD HELP ME IF YOU CAN! 451 | def stop_listening 452 | return unless Thread === @ioloop_thread 453 | 454 | # Do thread-ending in a new thread or else we're liable to kill the 455 | # thread that's called this method 456 | Thread.new do 457 | # Kill all threads if they're really threads 458 | [@ioloop_thread, @input_processor, @privmsg_processor].each {|thread| thread.terminate if Thread === thread} 459 | 460 | @socket.close 461 | @socket = nil 462 | @dead_socket = true 463 | 464 | @ioloop_thread = nil 465 | @input_processor = nil 466 | @privmsg_processor = nil 467 | end 468 | end 469 | 470 | private 471 | 472 | # Sets up all default filters and callbacks 473 | def set_defaults 474 | # Set up callbacks for slightly more important things than reporting - note that these should 475 | # eventually be changed as they don't belong in the core of YAIL. Note that since these are 476 | # callbacks, the user can very easily overwrite them, at least. 477 | on_nicknameinuse self.method(:_nicknameinuse) 478 | on_namreply self.method(:_namreply) 479 | 480 | # Set up truly core handlers/filters - these shouldn't be overridden unless users like to get 481 | # their hands dirty 482 | set_callback(:outgoing_begin_connection, self.method(:out_begin_connection)) 483 | on_ping self.method(:magic_ping) 484 | 485 | # Nick change magically setting @me is necessary as a filter - user can handle the event and do 486 | # anything he wants, but this should still run. 487 | hearing_nick self.method(:magic_nick) 488 | 489 | # Welcome magic also sets @me magically, so it's a filter 490 | hearing_welcome self.method(:magic_welcome) 491 | 492 | # Outgoing handlers are what make this app actually work - users who override these have to 493 | # do so very explicitly (no "on_xxx" magic) and will probably break stuff. Use filters instead! 494 | 495 | # These three need magic to buffer their output, so can't use our simpler create_command system 496 | set_callback :outgoing_msg, self.method(:magic_out_msg) 497 | set_callback :outgoing_ctcp, self.method(:magic_out_ctcp) 498 | set_callback :outgoing_act, self.method(:magic_out_act) 499 | 500 | # WHOIS is tricky due to how weird its argument positioning is, so can't use create_command, either 501 | set_callback :outgoing_whois, self.method(:magic_out_whois) 502 | 503 | # All PRIVMSG events eventually hit this - it's a legacy thing, and kinda dumb, but there you 504 | # have it. Just sends a raw PRIVMSG out to the socket. 505 | create_command :privmsg, "PRIVMSG :target ::message", :target, :message 506 | 507 | # The rest of these should be fairly obvious 508 | create_command :notice, "NOTICE :target ::message", :target, :message 509 | create_command :ctcpreply, "NOTICE :target :\001:message\001", :target, :message 510 | create_command :mode, "MODE", :target, " :target", :modes, " :modes", :objects, " :objects" 511 | create_command :join, "JOIN :channel", :channel, :password, " :password" 512 | create_command :part, "PART :channel", :channel, :message, " ::message" 513 | create_command :quit, "QUIT", :message, " ::message" 514 | create_command :nick, "NICK ::nick", :nick 515 | create_command :user, "USER :username :hostname :servername ::realname", :username, :hostname, :servername, :realname 516 | create_command :pass, "PASS :password", :password 517 | create_command :oper, "OPER :user :password", :user, :password 518 | create_command :topic, "TOPIC :channel", :channel, :topic, " ::topic" 519 | create_command :names, "NAMES", :channel, " :channel" 520 | create_command :list, "LIST", :channel, " :channel", :server, " :server" 521 | create_command :invite, "INVITE :nick :channel", :nick, :channel 522 | create_command :kick, "KICK :channel :nick", :nick, :channel, :message, " ::message" 523 | end 524 | 525 | # Prepares @socket for use and defaults @dead_socket to false 526 | def prepare_tcp_socket 527 | @dead_socket = false 528 | 529 | # Build our socket - if something goes wrong, it's immediately a dead socket. 530 | begin 531 | @socket = TCPSocket.new(@address, @port) 532 | setup_ssl if @ssl 533 | rescue StandardError => boom 534 | @log.fatal "+++ERROR: Unable to open socket connection in Net::YAIL.initialize: #{boom.inspect}" 535 | @dead_socket = true 536 | raise 537 | end 538 | end 539 | 540 | # If user asked for SSL, this is where we set it all up 541 | def setup_ssl 542 | ssl_context = OpenSSL::SSL::SSLContext.new() 543 | ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE 544 | @socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context) 545 | @socket.sync = true 546 | @socket.connect 547 | end 548 | 549 | # Depending on protocol (SSL vs. not), reads atomic messages from socket. This could be the 550 | # start of more generic message reading for other protocols, but for now reads a single line 551 | # for IRC and any number of lines from SSL IRC. 552 | def read_socket_messages 553 | # Simple non-ssl socket == return a single line 554 | return [@socket.gets] unless @ssl 555 | 556 | # SSL socket == return all lines available - but preserve newlines for servers that split 557 | # up words! Newlines tell us where commands end! 558 | messages = @socket.readpartial(OpenSSL::Buffering::BLOCK_SIZE) 559 | return messages.split(/(#{$/})/).each_slice(2).map(&:join) 560 | end 561 | 562 | # Reads incoming data - should only be called by io_loop, and only when 563 | # we've already ensured that data is, in fact, available. 564 | def read_incoming_data 565 | begin 566 | messages = read_socket_messages.compact 567 | rescue StandardError => boom 568 | @dead_socket = true 569 | @log.fatal "+++ERROR in read_incoming_data -> @socket.gets: #{boom.inspect}" 570 | raise 571 | end 572 | 573 | # If we somehow got no data here, the socket is closed. Run away!!! 574 | if !messages || messages.empty? 575 | @dead_socket = true 576 | return 577 | end 578 | 579 | # Chomp and push each message 580 | for message in messages 581 | # Message must have one of \r or \n at the end of it, otherwise it's a partial command and 582 | # we need to hang onto it to join it with the next message 583 | if message !~ /[\r\n]+$/ 584 | @prepend_message ||= "" 585 | @prepend_message += message.dup 586 | next 587 | end 588 | 589 | # If we had a partial message recently, attach it to the new message and clear it out 590 | if @prepend_message 591 | message = @prepend_message + message 592 | @prepend_message = nil 593 | end 594 | 595 | message.chomp! 596 | @log.debug "+++INCOMING: #{message.inspect}" 597 | 598 | # Only synchronize long enough to push our incoming string onto the 599 | # input buffer 600 | @input_buffer_mutex.synchronize do 601 | @input_buffer.push(message) 602 | end 603 | end 604 | end 605 | 606 | # This should be called from a thread only! Does nothing but listens 607 | # forever for incoming data, and calling filters/callback due to this listening 608 | def io_loop 609 | loop do 610 | # Possible fix for SSL one-message-behind issue from BP - thanks! 611 | # 612 | # If SSL, we just assume we're ready so we're always grabbing the latest message(s) from the 613 | # socket. I don't know if this will have any side-effects, but it seems to work in at least 614 | # one situation, sooo.... 615 | ready = true if @ssl 616 | unless ready 617 | # if no data is coming in, don't block the socket! To allow for mocked IO objects, allow 618 | # a non-IO to let us know if it's ready 619 | ready = @socket.kind_of?(IO) ? Kernel.select([@socket], nil, nil, 0) : @socket.ready? 620 | end 621 | 622 | read_incoming_data if ready 623 | 624 | # Check for dead socket 625 | @dead_socket = true if @socket.eof? 626 | 627 | sleep 0.05 628 | end 629 | end 630 | 631 | # This again is a thread-only method. Loops forever, handling input 632 | # whenever the @input_buffer var has any. 633 | def process_input_loop 634 | lines = nil 635 | loop do 636 | # Only synchronize long enough to copy and clear the input buffer. 637 | @input_buffer_mutex.synchronize do 638 | lines = @input_buffer.dup 639 | @input_buffer.clear 640 | end 641 | 642 | if lines 643 | # Now actually handle the data we copied, secure in the knowledge 644 | # that our reader thread is no longer going to wait on us. 645 | until lines.empty? 646 | event = Net::YAIL::IncomingEvent.parse(lines.shift) 647 | dispatch(event) 648 | end 649 | 650 | lines = nil 651 | end 652 | 653 | sleep 0.05 654 | end 655 | end 656 | 657 | # Grabs one message for each target in the private message buffer, removing 658 | # messages from @privmsg_buffer. Returns an array of events to process 659 | def pop_privmsgs 660 | privmsgs = [] 661 | 662 | # Only synchronize long enough to pop the appropriate messages. By 663 | # the way, this is UGLY! I should really move some of this stuff.... 664 | @privmsg_buffer_mutex.synchronize do 665 | for target in @privmsg_buffer.keys 666 | # Clean up our buffer to avoid a bunch of empty elements wasting 667 | # time and space 668 | if @privmsg_buffer[target].nil? || @privmsg_buffer[target].empty? 669 | @privmsg_buffer.delete(target) 670 | next 671 | end 672 | 673 | privmsgs.push @privmsg_buffer[target].shift 674 | end 675 | end 676 | 677 | return privmsgs 678 | end 679 | 680 | # Checks for new private messages, and dispatches all that are gathered from pop_privmsgs, if any 681 | def check_privmsg_output 682 | privmsgs = pop_privmsgs 683 | @next_message_time = Time.now + @throttle_seconds unless privmsgs.empty? 684 | privmsgs.each {|event| dispatch event} 685 | end 686 | 687 | # Our final thread loop - grabs the first privmsg for each target and 688 | # sends it on its way. 689 | def process_privmsg_loop 690 | loop do 691 | check_privmsg_output if @next_message_time <= Time.now && !@privmsg_buffer.empty? 692 | 693 | sleep 0.05 694 | end 695 | end 696 | 697 | ################################################## 698 | # EVENT HANDLING ULTRA SUPERSYSTEM DELUXE!!! 699 | ################################################## 700 | 701 | public 702 | # Prepends the given block or method to the before_filters array for the given type. Before-filters are called 703 | # before the event callback has run, and can stop the event (and other filters) from running by calling the event's 704 | # end_chain() method. Filters shouldn't do this very often! Before-filtering can modify output text before the 705 | # event callback runs, ignore incoming events for a given user, etc. 706 | def before_filter(event_type, method = nil, conditions = {}, &block) 707 | filter = block_given? ? block : method 708 | if filter 709 | event_type = numeric_event_type_convert(event_type) 710 | @before_filters[event_type] ||= Array.new 711 | @before_filters[event_type].unshift(Net::YAIL::Handler.new(filter, conditions)) 712 | end 713 | end 714 | 715 | # Sets up the callback for the given incoming event type. Note that unlike Net::YAIL 1.4.x and prior, there is no 716 | # longer a concept of multiple callbacks! Use filters for that kind of functionality. Think this way: the callback 717 | # is the action that takes place when an event hits. Filters are for functionality related to the event, but not 718 | # the definitive callback - logging, filtering messages, stats gathering, ignoring messages from a set user, etc. 719 | def set_callback(event_type, method = nil, conditions = {}, &block) 720 | callback = block_given? ? block : method 721 | event_type = numeric_event_type_convert(event_type) 722 | @callback[event_type] = Net::YAIL::Handler.new(callback, conditions) 723 | @callback.delete(event_type) unless callback 724 | end 725 | 726 | # Prepends the given block or method to the after_filters array for the given type. After-filters are called after 727 | # the event callback has run, and cannot stop other after-filters from running. Best used for logging or statistics 728 | # gathering. 729 | def after_filter(event_type, method = nil, conditions = {}, &block) 730 | filter = block_given? ? block : method 731 | if filter 732 | event_type = numeric_event_type_convert(event_type) 733 | @after_filters[event_type] ||= Array.new 734 | @after_filters[event_type].unshift(Net::YAIL::Handler.new(filter, conditions)) 735 | end 736 | end 737 | 738 | # Reports may not get printed in the proper order since I scrubbed the 739 | # IRCSocket report capturing, but this is way more straightforward to me. 740 | def report(*lines) 741 | @log.warn '[DEPRECATED] - Net::YAIL#report is deprecated and will be removed in 2.0 - use the logger (e.g., "@irc.log.info") instead' 742 | lines.each {|line| @log.info line} 743 | end 744 | 745 | # Converts events that are numerics into the internal "incoming_numeric_xxx" format 746 | def numeric_event_type_convert(type) 747 | if (type.to_s =~ /^incoming_(.*)$/) 748 | number = @event_number_lookup[$1].to_i 749 | type = :"incoming_numeric_#{number}" if number > 0 750 | end 751 | 752 | return type 753 | end 754 | 755 | # Handles magic listener setup methods: on_xxx, hearing_xxx, heard_xxx, saying_xxx, and said_xxx 756 | def method_missing(name, *args, &block) 757 | method = nil 758 | event_type = nil 759 | 760 | case name.to_s 761 | when /^on_(.*)$/ 762 | method = :set_callback 763 | event_type = :"incoming_#{$1}" 764 | 765 | when /^hearing_(.*)$/ 766 | method = :before_filter 767 | event_type = :"incoming_#{$1}" 768 | 769 | when /^heard_(.*)$/ 770 | method = :after_filter 771 | event_type = :"incoming_#{$1}" 772 | 773 | when /^saying_(.*)$/ 774 | method = :before_filter 775 | event_type = :"outgoing_#{$1}" 776 | 777 | when /^said_(.*)$/ 778 | method = :after_filter 779 | event_type = :"outgoing_#{$1}" 780 | end 781 | 782 | # Magic methods MUST have an arg or a block! 783 | filter_or_callback_method = block_given? ? block : args.shift 784 | conditions = args.shift || {} 785 | 786 | # If we didn't match a magic method signature, or we don't have the expected parameters, call 787 | # parent's method_missing. Just to be safe, we also return, in case YAIL one day subclasses 788 | # from something that handles some method_missing stuff. 789 | return super if method.nil? || event_type.nil? || args.length > 0 790 | 791 | self.send(method, event_type, filter_or_callback_method, conditions) 792 | end 793 | end 794 | 795 | end 796 | --------------------------------------------------------------------------------