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 =~ %r^(:(\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 =~ %r^#{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(%r(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params if command =~ %r^(\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] =~ %r([^@!\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(%r\S+/).each { |u| # FIXME beware of servers that allow multiple prefixes if(u =~ %r^([#{@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] =~ %r^-\s+(\S+)\s/ server = $1 else warning "Server doesn't have an RFC compliant MOTD start." end @motd = "" when RPL_MOTD if(argv[1] =~ %r^-\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] =~ %r^(H|G)(\*)?(.*)?$/ data[:away] = ($1 == 'G') data[:ircop] = $2 data[:modes] = $3.scan(%r./).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(%r[#{@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