Implements RFC 2812 and prior IRC RFCs.
Clients should register Proc{}s to handle the various server events, and the Client class will handle dispatch.
Create a new Client instance
# File lib/rbot/rfc2812.rb, line 961 def initialize @server = Server.new # The Server @user = @server.user("*!*@*") # The User representing the client on this Server @handlers = Hash.new # This is used by some messages to build lists of users that # will be delegated when the ENDOF... message is received @tmpusers = [] # Same as above, just for bans @tmpbans = [] end
key |
server event to handle |
value |
proc object called when event occurs |
set a handler for a server event
TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
welcome |
server welcome message on connect |
yourhost |
your host details (on connection) |
created |
when the server was started |
isupport |
information about what this server supports |
ping |
server pings you (default handler returns a pong) |
nicktaken |
you tried to change nick to one that's in use |
badnick |
you tried to change nick to one that's invalid |
topic |
someone changed the topic of a channel |
topicinfo |
on joining a channel or asking for the topic, tells you who set it and when |
names |
server sends list of channel members when you join |
motd |
server message of the day |
privmsg |
privmsg, the core of IRC, a message to you from someone |
public |
optionally instead of getting privmsg you can hook to only the public ones... |
msg |
or only the private ones, or both |
kick |
someone got kicked from a channel |
part |
someone left a channel |
quit |
someone quit IRC |
join |
someone joined a channel |
changetopic |
the topic of a channel changed |
invite |
you are invited to a channel |
nick |
someone changed their nick |
mode |
a mode change |
notice |
someone sends you a notice |
unknown |
any other message not handled by the above |
# File lib/rbot/rfc2812.rb, line 1015 def []=(key, value) @handlers[key] = value end
key |
event name |
remove a handler for a server event
# File lib/rbot/rfc2812.rb, line 1021 def deletehandler(key) @handlers.delete(key) end
takes a server string, checks for PING, PRIVMSG, NOTIFY, etc, and parses numeric server replies, calling the appropriate handler for each, and sending it a hash containing the data from the server
# File lib/rbot/rfc2812.rb, line 1028 def process(serverstring) data = Hash.new data[:serverstring] = serverstring unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/ raise "Unparseable Server Message!!!: #{serverstring.inspect}" end prefix, command, params = $2, $3, $5 if prefix != nil # Most servers will send a full nick!user@host prefix for # messages from users. Therefore, when the prefix doesn't match this # syntax it's usually the server hostname. # # This is not always true, though, since some servers do not send a # full hostmask for user messages. # if prefix =~ /^#{Regexp::Irc::BANG_AT}$/ data[:source] = @server.user(prefix) else if @server.hostname if @server.hostname != prefix # TODO do we want to be able to differentiate messages that are passed on to us from /other/ servers? debug "Origin #{prefix} for message\n\t#{serverstring.inspect}\nis neither a user hostmask nor the server hostname\nI'll pretend that it's from the server anyway" data[:source] = @server else data[:source] = @server end else @server.instance_variable_set(:@hostname, prefix) data[:source] = @server end end end # split parameters in an array argv = [] params.scan(/(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params if command =~ /^(\d+)$/ # Numeric replies data[:target] = argv[0] # A numeric reply /should/ be directed at the client, except when we're connecting with a used nick, in which case # it's directed at '*' not_us = !([@user.nick, '*'].include?(data[:target])) if not_us warning "Server reply #{serverstring.inspect} directed at #{data[:target]} instead of client (#{@user.nick})" end num=command.to_i case num when RPL_WELCOME data[:message] = argv[1] # "Welcome to the Internet Relay Network # <nick>!<user>@<host>" if not_us warning "Server thinks client (#{@user.inspect}) has a different nick" @user.nick = data[:target] end if data[:message] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/ nick = $1 user = $2 host = $3 warning "Welcome message nick mismatch (#{nick} vs #{data[:target]})" if nick != data[:target] @user.user = user if user @user.host = host if host end handle(:welcome, data) when RPL_YOURHOST # "Your host is <servername>, running version <ver>" data[:message] = argv[1] handle(:yourhost, data) when RPL_CREATED # "This server was created <date>" data[:message] = argv[1] handle(:created, data) when RPL_MYINFO # "<servername> <version> <available user modes> # <available channel modes>" @server.parse_my_info(params.split(' ', 2).last) data[:servername] = @server.hostname data[:version] = @server.version data[:usermodes] = @server.usermodes data[:chanmodes] = @server.chanmodes handle(:myinfo, data) when RPL_ISUPPORT # "PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server" # "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63 # TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=# # PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer :are available # on this server" # @server.parse_isupport(argv[1..-2].join(' ')) handle(:isupport, data) when ERR_NICKNAMEINUSE # "* <nick> :Nickname is already in use" data[:nick] = argv[1] data[:message] = argv[2] handle(:nicktaken, data) when ERR_ERRONEUSNICKNAME # "* <nick> :Erroneous nickname" data[:nick] = argv[1] data[:message] = argv[2] handle(:badnick, data) when RPL_TOPIC data[:channel] = @server.channel(argv[1]) data[:topic] = argv[2] data[:channel].topic.text = data[:topic] handle(:topic, data) when RPL_TOPIC_INFO data[:nick] = @server.user(argv[0]) data[:channel] = @server.channel(argv[1]) # This must not be an IRC::User because it might not be an actual User, # and we risk overwriting valid User data data[:source] = argv[2].to_irc_netmask(:server => @server) data[:time] = Time.at(argv[3].to_i) data[:channel].topic.set_by = data[:source] data[:channel].topic.set_on = data[:time] handle(:topicinfo, data) when RPL_NAMREPLY # "( "=" / "*" / "@" ) <channel> # :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> ) # - "@" is used for secret channels, "*" for private # channels, and "=" for others (public channels). data[:channeltype] = argv[1] data[:channel] = chan = @server.channel(argv[2]) users = [] argv[3].scan(/\S+/).each { |u| # FIXME beware of servers that allow multiple prefixes if(u =~ /^([#{@server.supports[:prefix][:prefixes].join}])?(.*)$/) umode = $1 user = $2 users << [user, umode] end } users.each { |ar| u = @server.user(ar[0]) chan.add_user(u, :silent => true) debug "Adding user #{u}" if ar[1] ms = @server.mode_for_prefix(ar[1].to_sym) debug "\twith mode #{ar[1]} (#{ms})" chan.mode[ms].set(u) end } @tmpusers += users when RPL_ENDOFNAMES data[:channel] = @server.channel(argv[1]) data[:users] = @tmpusers handle(:names, data) @tmpusers = Array.new when RPL_BANLIST data[:channel] = @server.channel(argv[1]) data[:mask] = argv[2] data[:by] = argv[3] data[:at] = argv[4] @tmpbans << data when RPL_ENDOFBANLIST data[:channel] = @server.channel(argv[1]) data[:bans] = @tmpbans handle(:banlist, data) @tmpbans = Array.new when RPL_LUSERCLIENT # ":There are <integer> users and <integer> # services on <integer> servers" data[:message] = argv[1] handle(:luserclient, data) when RPL_LUSEROP # "<integer> :operator(s) online" data[:ops] = argv[1].to_i handle(:luserop, data) when RPL_LUSERUNKNOWN # "<integer> :unknown connection(s)" data[:unknown] = argv[1].to_i handle(:luserunknown, data) when RPL_LUSERCHANNELS # "<integer> :channels formed" data[:channels] = argv[1].to_i handle(:luserchannels, data) when RPL_LUSERME # ":I have <integer> clients and <integer> servers" data[:message] = argv[1] handle(:luserme, data) when ERR_NOMOTD # ":MOTD File is missing" data[:message] = argv[1] handle(:motd_missing, data) when RPL_LOCALUSERS # ":Current local users: 3 Max: 4" data[:message] = argv[1] handle(:localusers, data) when RPL_GLOBALUSERS # ":Current global users: 3 Max: 4" data[:message] = argv[1] handle(:globalusers, data) when RPL_STATSCONN # ":Highest connection count: 4 (4 clients) (251 since server was # (re)started)" data[:message] = argv[1] handle(:statsconn, data) when RPL_MOTDSTART # "<nick> :- <server> Message of the Day -" if argv[1] =~ /^-\s+(\S+)\s/ server = $1 else warning "Server doesn't have an RFC compliant MOTD start." end @motd = "" when RPL_MOTD if(argv[1] =~ /^-\s+(.*)$/) @motd << $1 @motd << "\n" end when RPL_ENDOFMOTD data[:motd] = @motd handle(:motd, data) when RPL_DATASTR data[:text] = argv[1] handle(:datastr, data) when RPL_AWAY data[:nick] = user = @server.user(argv[1]) data[:message] = argv[-1] user.away = data[:message] handle(:away, data) when RPL_WHOREPLY data[:channel] = channel = @server.channel(argv[1]) data[:user] = argv[2] data[:host] = argv[3] data[:userserver] = argv[4] data[:nick] = user = @server.user(argv[5]) if argv[6] =~ /^(H|G)(\*)?(.*)?$/ data[:away] = ($1 == 'G') data[:ircop] = $2 data[:modes] = $3.scan(/./).map { |mode| m = @server.supports[:prefix][:prefixes].index(mode.to_sym) @server.supports[:prefix][:modes][m] } rescue [] else warning "Strange WHO reply: #{serverstring.inspect}" end data[:hopcount], data[:real_name] = argv[7].split(" ", 2) user.user = data[:user] user.host = data[:host] user.away = data[:away] # FIXME doesn't provide the actual message # TODO ircop status # TODO userserver # TODO hopcount user.real_name = data[:real_name] channel.add_user(user, :silent=>true) data[:modes].map { |mode| channel.mode[mode].set(user) } handle(:who, data) when RPL_ENDOFWHO handle(:eowho, data) when RPL_WHOISUSER @whois ||= Hash.new @whois[:nick] = argv[1] @whois[:user] = argv[2] @whois[:host] = argv[3] @whois[:real_name] = argv[-1] user = @server.user(@whois[:nick]) user.user = @whois[:user] user.host = @whois[:host] user.real_name = @whois[:real_name] when RPL_WHOISSERVER @whois ||= Hash.new @whois[:nick] = argv[1] @whois[:server] = argv[2] @whois[:server_info] = argv[-1] # TODO update user info when RPL_WHOISOPERATOR @whois ||= Hash.new @whois[:nick] = argv[1] @whois[:operator] = argv[-1] # TODO update user info when RPL_WHOISIDLE @whois ||= Hash.new @whois[:nick] = argv[1] user = @server.user(@whois[:nick]) @whois[:idle] = argv[2].to_i user.idle_since = Time.now - @whois[:idle] if argv[-1] == 'seconds idle, signon time' @whois[:signon] = Time.at(argv[3].to_i) user.signon = @whois[:signon] end when RPL_ENDOFWHOIS @whois ||= Hash.new @whois[:nick] = argv[1] data[:whois] = @whois.dup @whois.clear handle(:whois, data) when RPL_WHOISCHANNELS @whois ||= Hash.new @whois[:nick] = argv[1] @whois[:channels] ||= [] user = @server.user(@whois[:nick]) argv[-1].split.each do |prechan| pfx = prechan.scan(/[#{@server.supports[:prefix][:prefixes].join}]/) modes = pfx.map { |p| @server.mode_for_prefix p } chan = prechan[pfx.length..prechan.length] channel = @server.channel(chan) channel.add_user(user, :silent => true) modes.map { |mode| channel.mode[mode].set(user) } @whois[:channels] << [chan, modes] end when RPL_CHANNELMODEIS parse_mode(serverstring, argv[1..-1], data) handle(:mode, data) when RPL_CREATIONTIME data[:channel] = @server.channel(argv[1]) data[:time] = Time.at(argv[2].to_i) data[:channel].creation_time=data[:time] handle(:creationtime, data) when RPL_CHANNEL_URL data[:channel] = @server.channel(argv[1]) data[:url] = argv[2] data[:channel].url=data[:url].dup handle(:channel_url, data) when ERR_NOSUCHNICK data[:target] = argv[1] data[:message] = argv[2] handle(:nosuchtarget, data) if user = @server.get_user(data[:target]) @server.delete_user(user) end when ERR_NOSUCHCHANNEL data[:target] = argv[1] data[:message] = argv[2] handle(:nosuchtarget, data) if channel = @server.get_channel(data[:target]) @server.delete_channel(channel) end else warning "Unknown message #{serverstring.inspect}" handle(:unknown, data) end return # We've processed the numeric reply end # Otherwise, the command should be a single word case command.to_sym when :PING data[:pingid] = argv[0] handle(:ping, data) when :PONG data[:pingid] = argv[0] handle(:pong, data) when :PRIVMSG # you can either bind to 'PRIVMSG', to get every one and # parse it yourself, or you can bind to 'MSG', 'PUBLIC', # etc and get it all nicely split up for you. begin data[:target] = @server.user_or_channel(argv[0]) rescue # The previous may fail e.g. when the target is a server or something # like that (e.g. $<mask>). In any of these cases, we just use the # String as a target # FIXME we probably want to explicitly check for the #<mask> $<mask> data[:target] = argv[0] end data[:message] = argv[1] handle(:privmsg, data) # Now we split it if data[:target].kind_of?(Channel) handle(:public, data) else handle(:msg, data) end when :NOTICE begin data[:target] = @server.user_or_channel(argv[0]) rescue # The previous may fail e.g. when the target is a server or something # like that (e.g. $<mask>). In any of these cases, we just use the # String as a target # FIXME we probably want to explicitly check for the #<mask> $<mask> data[:target] = argv[0] end data[:message] = argv[1] case data[:source] when User handle(:notice, data) else # "server notice" (not from user, noone to reply to) handle(:snotice, data) end when :KICK data[:channel] = @server.channel(argv[0]) data[:target] = @server.user(argv[1]) data[:message] = argv[2] @server.delete_user_from_channel(data[:target], data[:channel]) if data[:target] == @user @server.delete_channel(data[:channel]) end handle(:kick, data) when :PART data[:channel] = @server.channel(argv[0]) data[:message] = argv[1] @server.delete_user_from_channel(data[:source], data[:channel]) if data[:source] == @user @server.delete_channel(data[:channel]) end handle(:part, data) when :QUIT data[:message] = argv[0] data[:was_on] = @server.channels.inject(ChannelList.new) { |list, ch| list << ch if ch.has_user?(data[:source]) list } @server.delete_user(data[:source]) handle(:quit, data) when :JOIN data[:channel] = @server.channel(argv[0]) data[:channel].add_user(data[:source]) handle(:join, data) when :TOPIC data[:channel] = @server.channel(argv[0]) data[:topic] = Channel::Topic.new(argv[1], data[:source], Time.new) data[:channel].topic.replace(data[:topic]) handle(:changetopic, data) when :INVITE data[:target] = @server.user(argv[0]) data[:channel] = @server.channel(argv[1]) handle(:invite, data) when :NICK data[:is_on] = @server.channels.inject(ChannelList.new) { |list, ch| list << ch if ch.has_user?(data[:source]) list } data[:newnick] = argv[0] data[:oldnick] = data[:source].nick.dup data[:source].nick = data[:newnick] debug "#{data[:oldnick]} (now #{data[:newnick]}) was on #{data[:is_on].join(', ')}" handle(:nick, data) when :MODE parse_mode(serverstring, argv, data) handle(:mode, data) when :ERROR data[:message] = argv[1] handle(:error, data) else warning "Unknown message #{serverstring.inspect}" handle(:unknown, data) end end
key |
server event name |
data |
hash containing data about the event, passed to the proc |
call client's proc for an event, if they set one as a handler
# File lib/rbot/rfc2812.rb, line 1507 def handle(key, data) if(@handlers.has_key?(key)) @handlers[key].call(data) end end
RPL_CHANNELMODEIS MODE ([+-]<modes> (<params>)*)* When a MODE message is received by a server, Type C will have parameters too, so we must be able to consume parameters for all but Type D modes
# File lib/rbot/rfc2812.rb, line 1519 def parse_mode(serverstring, argv, data) data[:target] = @server.user_or_channel(argv[0]) data[:modestring] = argv[1..-1].join(" ") # data[:modes] is an array where each element # is an array with two elements, the first of which # is either :set or :reset, and the second symbol # is the mode letter. An optional third element # is present e.g. for channel modes that need # a parameter data[:modes] = [] case data[:target] when User # User modes aren't currently handled internally, # but we still parse them and delegate to the client warning "Unhandled user mode message '#{serverstring}'" argv[1..-1].each { |arg| setting = arg[0].chr if "+-".include?(setting) setting = setting == "+" ? :set : :reset arg[1..-1].each_byte { |b| m = b.chr.intern data[:modes] << [setting, m] } else # Although typically User modes don't take an argument, # this is not true for all modes on all servers. Since # we have no knowledge of which modes take parameters # and which don't we just assign it to the last # mode. This is not going to do strange things often, # as usually User modes are only set one at a time warning "Unhandled user mode parameter #{arg} found" data[:modes].last << arg end } when Channel # array of indices in data[:modes] where parameters # are needed who_wants_params = [] modes = argv[1..-1].dup debug modes getting_args = false while arg = modes.shift debug arg if getting_args # getting args for previously set modes idx = who_wants_params.shift if idx.nil? warning "Oops, problems parsing #{serverstring.inspect}" break end data[:modes][idx] << arg getting_args = false if who_wants_params.empty? else debug @server.supports[:chanmodes] setting = :set arg.each_byte do |c| m = c.chr.intern case m when :+ setting = :set when :- setting = :reset else data[:modes] << [setting, m] case m when *@server.supports[:chanmodes][:typea] who_wants_params << data[:modes].length - 1 when *@server.supports[:chanmodes][:typeb] who_wants_params << data[:modes].length - 1 when *@server.supports[:chanmodes][:typec] if setting == :set who_wants_params << data[:modes].length - 1 end when *@server.supports[:chanmodes][:typed] # Nothing to do when *@server.supports[:prefix][:modes] who_wants_params << data[:modes].length - 1 else warning "Ignoring unknown mode #{m} in #{serverstring.inspect}" data[:modes].pop end end end getting_args = true unless who_wants_params.empty? end end unless who_wants_params.empty? warning "Unhandled malformed modeline #{data[:modestring]} (unexpected empty arguments)" return end data[:modes].each { |mode| set, key, val = mode if val data[:target].mode[key].send(set, val) else data[:target].mode[key].send(set) end } else warning "Ignoring #{data[:modestring]} for unrecognized target #{argv[0]} (#{data[:target].inspect})" end end
Generated with the Darkfish Rdoc Generator 2.