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.
the Server we're connected to
the User representing us on that server
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
server event to handle
proc object called when event occurs
set a handler for a server event
TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
server welcome message on connect
your host details (on connection)
when the server was started
information about what this server supports
server pings you (default handler returns a pong)
you tried to change nick to one that's in use
you tried to change nick to one that's invalid
someone changed the topic of a channel
on joining a channel or asking for the topic, tells you who set it and when
server sends list of channel members when you join
server message of the day
privmsg, the core of IRC, a message to you from someone
optionally instead of getting privmsg you can hook to only the public ones…
or only the private ones, or both
someone got kicked from a channel
someone left a channel
someone quit IRC
someone joined a channel
the topic of a channel changed
you are invited to a channel
someone changed their nick
a mode change
someone sends you a notice
any other message not handled by the above
# File lib/rbot/rfc2812.rb, line 1015 def []=(key, value) @handlers[key] = value end
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
Clear the server and reset the user
# File lib/rbot/rfc2812.rb, line 976 def reset @server.clear @user = @server.user("*!*@*") end
server event name
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