BotConfig.register BotConfigIntegerValue.new('core.save_every',
:default => 60, :validate => Proc.new{|v| v >= 0},
- # TODO change timer via on_change proc
+ :on_change => Proc.new { |bot, v|
+ if @save_timer
+ if v > 0
+ @timer.reschedule(@save_timer, v)
+ @timer.unblock(@save_timer)
+ else
+ @timer.block(@save_timer)
+ end
+ else
+ if v > 0
+ @save_timer = @timer.add(v) { bot.save }
+ end
+ # Nothing to do when v == 0
+ end
+ },
:desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)")
BotConfig.register BotConfigBooleanValue.new('core.run_as_daemon',
@timer = Timer::Timer.new(1.0) # only need per-second granularity
@save_mutex = Mutex.new
- @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
+ if @config['core.save_every'] > 0
+ @save_timer = @timer.add(@config['core.save_every']) { save }
+ else
+ @save_timer = nil
+ end
@quit_mutex = Mutex.new
@logs = Hash.new
end
}
}
+
+ # TODO the next two @client should go into rfc2812.rb, probably
+ # Since capabs are two-steps processes, server.supports[:capab]
+ # should be a three-state: nil, [], [....]
+ asked_for = { :"identify-msg" => false }
@client[:isupport] = proc { |data|
- # TODO this needs to go into rfc2812.rb
- # Since capabs are two-steps processes, server.supports[:capab]
- # should be a three-state: nil, [], [....]
- sendq "CAPAB IDENTIFY-MSG" if server.supports[:capab]
+ if server.supports[:capab] and !asked_for[:"identify-msg"]
+ sendq "CAPAB IDENTIFY-MSG"
+ asked_for[:"identify-msg"] = true
+ end
}
@client[:datastr] = proc { |data|
- # TODO this needs to go into rfc2812.rb
if data[:text] == "IDENTIFY-MSG"
- server.capabilities["identify-msg".to_sym] = true
+ server.capabilities[:"identify-msg"] = true
else
debug "Not handling RPL_DATASTR #{data[:servermessage]}"
end
}
+
@client[:privmsg] = proc { |data|
m = PrivMessage.new(self, server, data[:source], data[:target], data[:message])
# debug "Message source is #{data[:source].inspect}"
# debug "Message target is #{data[:target].inspect}"
# debug "Bot is #{myself.inspect}"
- # TODO use the new Netmask class
- # @config['irc.ignore_users'].each { |mask| return if Irc.netmaskmatch(mask,m.source) }
+ ignored = false
+ @config['irc.ignore_users'].each { |mask|
+ if m.source.matches?(server.new_netmask(mask))
+ ignored = true
+ break
+ end
+ }
irclogprivmsg(m)
- @plugins.delegate "listen", m
- @plugins.privmsg(m) if m.address?
+ unless ignored
+ @plugins.delegate "listen", m
+ @plugins.privmsg(m) if m.address?
+ end
}
@client[:notice] = proc { |data|
message = NoticeMessage.new(self, server, data[:source], data[:target], data[:message])
debug "my nick is now #{new}"
end
data[:is_on].each { |ch|
- irclog "@ #{old} is now known as #{new}", ch
+ irclog "@ #{old} is now known as #{new}", ch
}
@plugins.delegate("listen", m)
@plugins.delegate("nick", m)
#debug "UNKNOWN: #{data[:serverstring]}"
irclog data[:serverstring], ".unknown"
}
+
+ set_default_send_options
+ end
+
+ def set_default_send_options
+ # Default send options for NOTICE and PRIVMSG
+ # TODO document, for plugin writers
+ # TODO some of these options, like :truncate_text and :max_lines,
+ # should be made into config variables that trigger this routine on change
+ @default_send_options = {
+ :queue_channel => nil, # use default queue channel
+ :queue_ring => nil, # use default queue ring
+ :newlines => :split, # or :join
+ :join_with => ' ', # by default, use a single space
+ :max_lines => nil, # maximum number of lines to send with a single command
+ :overlong => :split, # or :truncate
+ # TODO an array of splitpoints would be preferrable for this option:
+ :split_at => /\s+/, # by default, split overlong lines at whitespace
+ :purge_split => true, # should the split string be removed?
+ :truncate_text => "#{Reverse}...#{Reverse}" # text to be appened when truncating
+ }
end
# checks if we should be quiet on a channel
# Type can be PRIVMSG, NOTICE, etc, but those you should really use the
# relevant say() or notice() methods. This one should be used for IRCd
# extensions you want to use in modules.
- def sendmsg(type, where, message, chan=nil, ring=0)
- # Split the message so that each line sent is not longher than 400 bytes
- # TODO allow something to do for commands that produce too many messages
- # TODO example: math 10**10000
- # TODO try to use the maximum line length allowed by the server, if there is
- # a way to know what it is
- left = 400 - type.length - where.to_s.length - 3
- begin
- if(left >= message.length)
- sendq "#{type} #{where} :#{message}", chan, ring
- log_sent(type, where, message)
- return
- end
- line = message.slice!(0, left)
- lastspace = line.rindex(/\s+/)
- if(lastspace)
- message = line.slice!(lastspace, line.length) + message
- message.gsub!(/^\s+/, "")
- end
- sendq "#{type} #{where} :#{line}", chan, ring
- log_sent(type, where, line)
- end while(message.length > 0)
- end
+ def sendmsg(type, where, original_message, options={})
+ opts = @default_send_options.merge(options)
- # queue an arbitraty message for the server
- def sendq(message="", chan=nil, ring=0)
- # temporary
- @socket.queue(message, chan, ring)
- end
-
- # send a notice message to channel/nick +where+
- def notice(where, message, mchan="", mring=-1)
- if mchan == ""
- chan = where
- else
+ # For starters, set up appropriate queue channels and rings
+ mchan = opts[:queue_channel]
+ mring = opts[:queue_ring]
+ if mchan
chan = mchan
+ else
+ chan = where
end
- if mring < 0
+ if mring
+ ring = mring
+ else
case where
when User
ring = 1
else
ring = 2
end
- else
- ring = mring
end
- message.each_line { |line|
- line.chomp!
- next unless(line.length > 0)
- sendmsg "NOTICE", where, line, chan, ring
- }
- end
- # say something (PRIVMSG) to channel/nick +where+
- def say(where, message, mchan="", mring=-1)
- if mchan == ""
- chan = where
+ message = original_message.to_s.gsub(/[\r\n]+/, "\n")
+ case opts[:newlines]
+ when :join
+ lines = [message.gsub("\n", opts[:join_with])]
+ when :split
+ lines = Array.new
+ message.each_line { |line|
+ line.chomp!
+ next unless(line.length > 0)
+ lines << line
+ }
else
- chan = mchan
+ raise "Unknown :newlines option #{opts[:newlines]} while sending #{original_message.inspect}"
end
- if mring < 0
- case where
- when User
- ring = 1
- else
- ring = 2
- end
+
+ # The IRC protocol requires that each raw message must be not longer
+ # than 512 characters. From this length with have to subtract the EOL
+ # terminators (CR+LF) and the length of ":botnick!botuser@bothost "
+ # that will be prepended by the server to all of our messages.
+
+ # The maximum raw message length we can send is therefore 512 - 2 - 2
+ # minus the length of our hostmask.
+
+ max_len = 508 - myself.fullform.length
+
+ # On servers that support IDENTIFY-MSG, we have to subtract 1, because messages
+ # will have a + or - prepended
+ if server.capabilities[:"identify-msg"]
+ max_len -= 1
+ end
+
+ # When splitting the message, we'll be prefixing the following string:
+ # (e.g. "PRIVMSG #rbot :")
+ fixed = "#{type} #{where} :"
+
+ # And this is what's left
+ left = max_len - fixed.length
+
+ case opts[:overlong]
+ when :split
+ truncate = false
+ split_at = opts[:split_at]
+ when :truncate
+ truncate = opts[:truncate_text]
+ truncate = @default_send_options[:truncate_text] if truncate.length > left
+ truncate = "" if truncate.length > left
else
- ring = mring
+ raise "Unknown :overlong option #{opts[:overlong]} while sending #{original_message.inspect}"
end
- message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line|
- line.chomp!
- next unless(line.length > 0)
- unless quiet_on?(where)
- sendmsg "PRIVMSG", where, line, chan, ring
- end
+
+ # Counter to check the number of lines sent by this command
+ cmd_lines = 0
+ max_lines = opts[:max_lines]
+ line = String.new
+ lines.each { |msg|
+ begin
+ if(left >= msg.length)
+ sendq "#{fixed}#{msg}", chan, ring
+ log_sent(type, where, msg)
+ return
+ end
+ if opts[:max_lines] and cmd_lines == max_lines - 1
+ debug "Max lines count reached for message #{original_message.inspect} while sending #{msg.inspect}, truncating"
+ truncate = opts[:truncate_text]
+ truncate = @default_send_options[:truncate_text] if truncate.length > left
+ truncate = "" if truncate.length > left
+ end
+ if truncate
+ line.replace msg.slice(0, left-truncate.length)
+ line.sub!(/\s+\S*$/, truncate)
+ raise "PROGRAMMER ERROR! #{line.inspect} of length #{line.length} > #{left}" if line.length > left
+ sendq "#{fixed}#{line}", chan, ring
+ log_sent(type, where, line)
+ return
+ end
+ line.replace msg.slice!(0, left)
+ lastspace = line.rindex(opts[:split_at])
+ if(lastspace)
+ msg.replace line.slice!(lastspace, line.length) + msg
+ msg.gsub!(/^#{opts[:split_at]}/, "") if opts[:purge_split]
+ end
+ sendq "#{fixed}#{line}", chan, ring
+ log_sent(type, where, line)
+ cmd_lines += 1
+ end while(msg.length > 0)
}
end
+ # queue an arbitraty message for the server
+ def sendq(message="", chan=nil, ring=0)
+ # temporary
+ @socket.queue(message, chan, ring)
+ end
+
+ # send a notice message to channel/nick +where+
+ def notice(where, message, options={})
+ unless quiet_on?(where)
+ sendmsg "NOTICE", where, message, options
+ end
+ end
+
+ # say something (PRIVMSG) to channel/nick +where+
+ def say(where, message, options={})
+ unless quiet_on?(where)
+ sendmsg "PRIVMSG", where, message, options
+ end
+ end
+
# perform a CTCP action with message +message+ to channel/nick +where+
- def action(where, message, mchan="", mring=-1)
- if mchan == ""
- chan = where
- else
+ def action(where, message, options={})
+ mchan = options.fetch(:queue_channel, nil)
+ mring = options.fetch(:queue_ring, nil)
+ if mchan
chan = mchan
+ else
+ chan = where
end
- if mring < 0
+ if mring
+ ring = mring
+ else
case where
- when Channel
- ring = 2
- else
+ when User
ring = 1
+ else
+ ring = 2
end
- else
- ring = mring
end
+ # FIXME doesn't check message length. Can we make this exploit sendmsg?
sendq "PRIVMSG #{where} :\001ACTION #{message}\001", chan, ring
case where
when Channel
# attempt to change bot's nick to +name+
def nickchg(name)
- sendq "NICK #{name}"
+ sendq "NICK #{name}"
end
# changing mode
def mode(channel, mode, target)
- sendq "MODE #{channel} #{mode} #{target}", channel, 2
+ sendq "MODE #{channel} #{mode} #{target}", channel, 2
end
# kicking a user
def kick(channel, user, msg)
- sendq "KICK #{channel} #{user} :#{msg}", channel, 2
+ sendq "KICK #{channel} #{user} :#{msg}", channel, 2
end
# m:: message asking for help
when Channel
irclog "-=#{myself}=- #{message}", where
else
- irclog "[-=#{where}=-] #{message}", where
+ irclog "[-=#{where}=-] #{message}", where
end
when "PRIVMSG"
case where