-class ::String
- # Calculate the penalty which will be assigned to this message
- # by the IRCd
- def irc_send_penalty
- # According to eggrdop, the initial penalty is
- penalty = 1 + self.length/100
- # on everything but UnderNET where it's
- # penalty = 2 + self.length/120
-
- cmd, pars = self.split($;,2)
- debug "cmd: #{cmd}, pars: #{pars.inspect}"
- case cmd.to_sym
- when :KICK
- chan, nick, msg = pars.split
- chan = chan.split(',')
- nick = nick.split(',')
- penalty += nick.length
- penalty *= chan.length
- when :MODE
- chan, modes, argument = pars.split
- extra = 0
- if modes
- extra = 1
- if argument
- extra += modes.split(/\+|-/).length
- else
- extra += 3 * modes.split(/\+|-/).length
- end
- end
- if argument
- extra += 2 * argument.split.length
- end
- penalty += extra * chan.split.length
- when :TOPIC
- penalty += 1
- penalty += 2 unless pars.split.length < 2
- when :PRIVMSG, :NOTICE
- dests = pars.split($;,2).first
- penalty += dests.split(',').length
- when :WHO
- # I'm too lazy to implement this one correctly
- penalty += 5
- when :AWAY, :JOIN, :VERSION, :TIME, :TRACE, :WHOIS, :DNS
- penalty += 2
- when :INVITE, :NICK
- penalty += 3
- when :ISON
- penalty += 1
- else # Unknown messages
- penalty += 1
- end
- if penalty > 99
- debug "Wow, more than 99 secs of penalty!"
- penalty = 99
- end
- if penalty < 2
- debug "Wow, less than 2 secs of penalty!"
- penalty = 2
- end
- debug "penalty: #{penalty}"
- return penalty
- end
-end
-
module Irc
# RFC 2812 Internet Relay Chat: Client Protocol
#
RPL_DATASTR=290
# implements RFC 2812 and prior IRC RFCs.
- # clients register handler proc{}s for different server events and IrcClient
+ # clients register handler proc{}s for different server events and Client
# handles dispatch
- class IrcClient
+ class Client
- attr_reader :server, :client
+ attr_reader :server, :user
- # create a new IrcClient instance
+ # create a new Client instance
def initialize
@server = Server.new # The Server
- @client = @server.user("") # The User representing the client on this Server
+ @user = @server.user("") # The User representing the client on this Server
@handlers = Hash.new
@tmpusers = []
end
+ # clear the server and reset the User
+ def reset
+ @server.clear
+ @user = @server.user("")
+ end
+
# key:: server event to handle
# value:: proc object called when event occurs
# set a handler for a server event
#
# ==server events currently supported:
#
+ # TODO handle errors ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL
+ # 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
data = Hash.new
data[:serverstring] = serverstring
- unless serverstring =~ /^(:(\S+)\s)?(\S+)(\s(.*))?/
- raise "Unparseable Server Message!!!: #{serverstring}"
+ unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/
+ raise "Unparseable Server Message!!!: #{serverstring.inspect}"
end
prefix, command, params = $2, $3, $5
if prefix != nil
- data[:source] = prefix
- if prefix =~ /^(\S+)!(\S+)$/
+ # 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 && @server.hostname != data[:source]
- warning "Unknown origin #{data[:source]} for message\n#{serverstring.inspect}"
+ 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, data[:source])
+ @server.instance_variable_set(:@hostname, prefix)
+ data[:source] = @server
end
- data[:source] = @server
end
end
argv = []
params.scan(/(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params
- case command
- when 'PING'
- data[:pingid] = argv[0]
- handle(:ping, data)
- when 'PONG'
- data[:pingid] = argv[0]
- handle(:pong, data)
- when /^(\d+)$/ # numerical server message
+ 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
# "Welcome to the Internet Relay Network
# <nick>!<user>@<host>"
- case argv[1]
- when /((\S+)!(\S+))/
- data[:netmask] = $1
- data[:nick] = $2
- data[:address] = $3
- @client = @server.user(data[:netmask])
- set = true
- when /Welcome to the Internet Relay Network\s(\S+)/
- data[:nick] = $1
- when /Welcome.*\s+(\S+)$/
- data[:nick] = $1
- when /^(\S+)$/
- data[:nick] = $1
+ if not_us
+ warning "Server thinks client (#{@user.inspect}) has a different nick"
+ @user.nick = data[:target]
+ end
+ if argv[1] =~ /([^@!\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
- @client = @server.user(data[:nick]) unless set
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>"
when RPL_TOPIC_INFO
data[:nick] = @server.user(argv[0])
data[:channel] = @server.get_channel(argv[1])
- data[:source] = @server.user(argv[2])
+
+ # 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)
if data[:channel]
- data[:channel].topic.set_by = data[:nick]
+ data[:channel].topic.set_by = data[:source]
data[:channel].topic.set_on = data[:time]
else
warning "Received topic #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
users = []
argv[3].scan(/\S+/).each { |u|
# FIXME beware of servers that allow multiple prefixes
- if(u =~ /^(#{@server.supports[:prefix][:prefixes].join})?(.*)$/)
+ if(u =~ /^([#{@server.supports[:prefix][:prefixes].join}])?(.*)$/)
umode = $1
user = $2
users << [user, umode]
users.each { |ar|
u = @server.user(ar[0])
- chan.users << u unless chan.users.include?(u)
+ chan.add_user(u, :silent => true)
+ debug "Adding user #{u}"
if ar[1]
m = @server.supports[:prefix][:prefixes].index(ar[1].to_sym)
- m = @server.supports[:prefix][:modes][m]
- chan.mode[m.to_sym].set(u)
+ ms = @server.supports[:prefix][:modes][m]
+ debug "\twith mode #{ar[1]} (#{ms})"
+ chan.mode[ms].set(u)
end
}
@tmpusers += users
else
handle(:unknown, data)
end
- # end of numeric replies
- when 'PRIVMSG'
+ 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.
else
handle(:msg, data)
end
- when 'NOTICE'
+ when :NOTICE
begin
data[:target] = @server.user_or_channel(argv[0])
rescue
# "server notice" (not from user, noone to reply to)
handle(:snotice, data)
end
- when 'KICK'
+ 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] == @client
+ if data[:target] == @user
@server.delete_channel(data[:channel])
end
handle(:kick, data)
- when 'PART'
+ when :PART
data[:channel] = @server.channel(argv[0])
data[:message] = argv[1]
@server.delete_user_from_channel(data[:source], data[:channel])
- if data[:source] == @client
+ if data[:source] == @user
@server.delete_channel(data[:channel])
end
handle(:part, data)
- when 'QUIT'
+ when :QUIT
data[:message] = argv[0]
data[:was_on] = @server.channels.inject(ChannelList.new) { |list, ch|
- list << ch if ch.users.include?(data[:source])
- list
+ list << ch if ch.has_user?(data[:source])
+ list
}
@server.delete_user(data[:source])
handle(:quit, data)
- when 'JOIN'
+ when :JOIN
data[:channel] = @server.channel(argv[0])
- data[:channel].users << data[:source]
+ data[:channel].add_user(data[:source])
handle(:join, data)
- when 'TOPIC'
+ 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'
+ when :INVITE
data[:target] = @server.user(argv[0])
data[:channel] = @server.channel(argv[1])
handle(:invite, data)
- when 'NICK'
+ when :NICK
data[:is_on] = @server.channels.inject(ChannelList.new) { |list, ch|
- list << ch if ch.users.include?(data[:source])
+ list << ch if ch.has_user?(data[:source])
list
}
debug "#{data[:oldnick]} (now #{data[:newnick]}) was on #{data[:is_on].join(', ')}"
handle(:nick, data)
- when 'MODE'
+ when :MODE
# MODE ([+-]<modes> (<params>)*)*
# When a MODE message is received by a server,
# Type C will have parameters too, so we must
case data[:channel]
when User
# TODO
+ warning "Unhandled user mode message '#{serverstring}'"
else
# data[:modes] is an array where each element
# is either a flag which doesn't need parameters
data[:modes] << [setting + m]
who_wants_params << data[:modes].length - 1
else
- warn "Unknown mode #{m} in #{serverstring}"
+ warning "Unknown mode #{m} in #{serverstring.inspect}"
end
}
else
idx = who_wants_params.shift
if idx.nil?
- warn "Oops, problems parsing #{serverstring}"
+ warning "Oops, problems parsing #{serverstring.inspect}"
break
end
data[:modes][idx] << arg
handle(:mode, data)
else
+ warning "Unknown message #{serverstring.inspect}"
handle(:unknown, data)
end
end