4 # :title: IRC message datastructures
11 Config.register ArrayValue.new('core.address_prefix',
12 :default => [], :wizard => true,
13 :desc => "what non nick-matching prefixes should the bot respond to as if addressed (e.g !, so that '!foo' is treated like 'rbot: foo')"
16 Config.register BooleanValue.new('core.reply_with_nick',
17 :default => false, :wizard => true,
18 :desc => "if true, the bot will prepend the nick to what he has to say when replying (e.g. 'markey: you can't do that!')"
21 Config.register StringValue.new('core.nick_postfix',
22 :default => ':', :wizard => true,
23 :desc => "when replying with nick put this character after the nick of the user the bot is replying to"
25 Config.register BooleanValue.new('core.private_replies',
27 :desc => 'Should the bot reply to private instead of the channel?'
33 # Define standard IRC attributes (not so standard actually,
34 # but the closest thing we have ...)
40 AttributeRx = /#{Bold}|#{Underline}|#{Reverse}|#{Italic}|#{NormalText}/
42 # Color is prefixed by \003 and followed by optional
43 # foreground and background specifications, two-digits-max
44 # numbers separated by a comma. One of the two parts
47 ColorRx = /#{Color}\d?\d?(?:,\d\d?)?/
49 FormattingRx = /#{AttributeRx}|#{ColorRx}/
51 # Standard color codes
78 # Convert a String or Symbol into a color number
79 def Irc.find_color(data)
80 "%02d" % if Integer === data
83 f = if String === data
96 # Insert the full color code for a given
97 # foreground/background combination.
98 def Irc.color(fg=nil,bg=nil)
101 str << Irc.find_color(fg)
104 str << "," << Irc.find_color(bg)
109 # base user message class, all user messages derive from this
110 # (a user message is defined as having a source hostmask, a target
111 # nick/channel and a message part)
112 class BasicUserMessage
120 # when the message was received
123 # User that originated the message
126 # User/Channel message was sent to
129 # contents of the message (stripped of initial/final format codes)
130 attr_accessor :message
132 # contents of the message (for logging purposes)
133 attr_accessor :logmessage
135 # contents of the message (stripped of all formatting)
136 attr_accessor :plainmessage
138 # has the message been replied to/handled by a plugin?
139 attr_accessor :replied
140 alias :replied? :replied
142 # should the message be ignored?
143 attr_accessor :ignored
144 alias :ignored? :ignored
146 # set this to true if the method that delegates the message is run in a thread
147 attr_accessor :in_thread
148 alias :in_thread? :in_thread
150 def inspect(fields=nil)
151 ret = self.__to_s__[0..-2]
152 ret << ' bot=' << @bot.__to_s__
153 ret << ' server=' << server.to_s
154 ret << ' time=' << time.to_s
155 ret << ' source=' << source.to_s
156 ret << ' target=' << target.to_s
157 ret << ' message=' << message.inspect
158 ret << ' logmessage=' << logmessage.inspect
159 ret << ' plainmessage=' << plainmessage.inspect
160 ret << fields if fields
161 ret << ' (identified)' if identified?
163 ret << ' (addressed to me'
164 ret << ', with prefix' if prefixed?
167 ret << ' (replied)' if replied?
168 ret << ' (ignored)' if ignored?
169 ret << ' (in thread)' if in_thread?
173 # instantiate a new Message
174 # bot:: associated bot class
175 # server:: Server where the message took place
176 # source:: User that sent the message
177 # target:: User/Channel is destined for
178 # message:: actual message
179 def initialize(bot, server, source, target, message)
180 @msg_wants_id = false unless defined? @msg_wants_id
188 @message = message || ""
195 if @msg_wants_id && @server.capabilities[:"identify-msg"]
196 if @message =~ /^([-+])(.*)/
197 @identified = ($1=="+")
200 warning "Message does not have identification"
203 @logmessage = @message.dup
204 @plainmessage = BasicUserMessage.strip_formatting(@message)
205 @message = BasicUserMessage.strip_initial_formatting(@message)
207 if target && target == @bot.myself
213 # Access the nick of the source
216 @source.nick rescue @source.to_s
219 # Access the user@host of the source
222 "#{@source.user}@#{@source.host}" rescue @source.to_s
225 # Access the botuser corresponding to the source, if any
228 source.botuser rescue @bot.auth.everyone
232 # Was the message from an identified user?
237 # returns true if the message was addressed to the bot.
238 # This includes any private message to the bot, or any public message
239 # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
240 # a kick message when bot was kicked etc.
245 # returns true if the messaged was addressed to the bot via the address
246 # prefix. This can be used to tell appart "!do this" from "botname, do this"
251 # strip mIRC colour escapes from a string
252 def BasicUserMessage.stripcolour(string)
253 return "" unless string
254 ret = string.gsub(ColorRx, "")
255 #ret.tr!("\x00-\x1f", "")
259 def BasicUserMessage.strip_initial_formatting(string)
260 return "" unless string
261 ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
264 def BasicUserMessage.strip_formatting(string)
265 string.gsub(FormattingRx,"")
270 # class for handling welcome messages from the server
271 class WelcomeMessage < BasicUserMessage
274 # class for handling MOTD from the server. Yes, MotdMessage
275 # is somewhat redundant, but it fits with the naming scheme
276 class MotdMessage < BasicUserMessage
279 # class for handling IRC user messages. Includes some utilities for handling
280 # the message, for example in plugins.
281 # The +message+ member will have any bot addressing "^bot: " removed
282 # (address? will return true in this case)
283 class UserMessage < BasicUserMessage
286 fields = ' plugin=' << plugin.inspect
287 fields << ' params=' << params.inspect
288 fields << ' channel=' << channel.to_s if channel
289 fields << ' (reply to ' << replyto.to_s << ')'
291 fields << ' (private)'
293 fields << ' (public)'
296 fields << ' (action)'
298 fields << ' (CTCP ' << ctcp << ')'
303 # for plugin messages, the name of the plugin invoked by the message
306 # for plugin messages, the rest of the message, with the plugin name
310 # convenience member. Who to reply to (i.e. would be sourcenick for a
311 # privately addressed message, or target (the channel) for a publicly
315 # channel the message was in, nil for privately addressed messages
318 # for PRIVMSGs, false unless the message was a CTCP command,
319 # in which case it evaluates to the CTCP command itself
320 # (TIME, PING, VERSION, etc). The CTCP command parameters
321 # are then stored in the message.
324 # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
325 # will be stripped from the message)
328 # instantiate a new UserMessage
329 # bot:: associated bot class
330 # source:: hostmask of the message source
331 # target:: nick/channel message is destined for
332 # message:: message part
333 def initialize(bot, server, source, target, message)
334 super(bot, server, source, target, message)
341 if target == @bot.myself
351 # check for option extra addressing prefixes, e.g "|search foo", or
352 # "!version" - first match wins
353 bot.config['core.address_prefix'].each {|mprefix|
354 if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
361 # even if they used above prefixes, we allow for silly people who
362 # combine all possible types, e.g. "|rbot: hello", or
363 # "/msg rbot rbot: hello", etc
364 if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
368 if(@message =~ /^\001(\S+)(\s(.+))?\001/)
370 # FIXME need to support quoting of NULL and CR/LF, see
371 # http://www.irchelp.org/irchelp/rfc/ctcpspec.html
372 @message = $3 || String.new
373 @action = @ctcp == 'ACTION'
374 debug "Received CTCP command #{@ctcp} with options #{@message} (action? #{@action})"
375 @logmessage = @message.dup
376 @plainmessage = BasicUserMessage.strip_formatting(@message)
377 @message = BasicUserMessage.strip_initial_formatting(@message)
380 # free splitting for plugins
381 @params = @message.dup
382 # Created messges (such as by fake_message) can contain multiple lines
383 if @params.gsub!(/\A\s*(\S+)[\s$]*/m, "")
384 @plugin = $1.downcase
385 @params = nil unless @params.length > 0
389 # returns true for private messages, e.g. "/msg bot hello"
394 # returns true if the message was in a channel
403 # convenience method to reply to a message, useful in plugins. It's the
405 # <tt>@bot.say m.replyto, string</tt>
406 # So if the message is private, it will reply to the user. If it was
407 # in a channel, it will reply in the channel.
408 def plainreply(string, options={})
409 reply string, {:nick => false}.merge(options)
412 # Same as reply, but when replying in public it adds the nick of the user
413 # the bot is replying to
414 def nickreply(string, options={})
415 reply string, {:nick => true}.merge(options)
418 # Same as nickreply, but always prepend the target's nick.
419 def nickreply!(string, options={})
420 reply string, {:nick => true, :forcenick => true}.merge(options)
423 # The general way to reply to a command. The following options are available:
424 # :nick [false, true, :auto]
425 # state if the nick of the user calling the command should be prepended
426 # :auto uses core.reply_with_nick
428 # :forcenick [false, true]
429 # if :nick is true, always prepend the target's nick, even if the nick
430 # already appears in the reply. Defaults to false.
432 # :to [:private, :public, :auto]
433 # where should the bot reply?
434 # :private always reply to the nick
435 # :public reply to the channel (if available)
436 # :auto uses core.private_replies
437 def reply(string, options={})
438 opts = {:nick => :auto, :forcenick => false, :to => :auto}.merge options
440 if opts[:nick] == :auto
441 opts[:nick] = @bot.config['core.reply_with_nick']
446 elsif opts[:to] == :auto
447 opts[:to] = @bot.config['core.private_replies'] ? :private : :public
451 opts[:to] != :private &&
452 (string !~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/ ||
454 string = "#{@source}#{@bot.config['core.nick_postfix']} #{string}"
456 to = (opts[:to] == :private) ? source : @channel
457 @bot.say to, string, options
461 # convenience method to reply to a message with an action. It's the
463 # <tt>@bot.action m.replyto, string</tt>
464 # So if the message is private, it will reply to the user. If it was
465 # in a channel, it will reply in the channel.
466 def act(string, options={})
467 @bot.action @replyto, string, options
471 # send a CTCP response, i.e. a private NOTICE to the sender
472 # with the same CTCP command and the reply as a parameter
473 def ctcp_reply(string, options={})
474 @bot.ctcp_notice @source, @ctcp, string, options
477 # convenience method to reply a literal message in the current language to the message
478 def plain_literal(ident)
479 self.reply @bot.lang.get(ident), :nick => false
482 # Like the above, but append the username
483 def nick_literal(ident)
484 str = @bot.lang.get(ident).dup
486 # remove final punctuation
487 str.gsub!(/[!,.]$/,"")
488 str += ", #{@source}"
490 self.reply str, :nick => false
493 # the default okay style is the same as the default reply style
495 @bot.config['core.reply_with_nick'] ? nick_literal('okay') : plain_literal('okay')
498 # thanks the user in reply
500 @bot.config['core.reply_with_nick'] ? nick_literal('thanks') : plain_literal('thanks')
503 # send a NOTICE to the message source
505 def notify(msg,opts={})
506 @bot.notice(sourcenick, msg, opts)
511 # class to manage IRC PRIVMSGs
512 class PrivMessage < UserMessage
513 def initialize(bot, server, source, target, message, opts={})
514 @msg_wants_id = opts[:handle_id]
515 super(bot, server, source, target, message)
519 # class to manage IRC NOTICEs
520 class NoticeMessage < UserMessage
521 def initialize(bot, server, source, target, message, opts={})
522 @msg_wants_id = opts[:handle_id]
523 super(bot, server, source, target, message)
527 # class to manage IRC KICKs
528 # +address?+ can be used as a shortcut to see if the bot was kicked,
529 # basically, +target+ was kicked from +channel+ by +source+ with +message+
530 class KickMessage < BasicUserMessage
531 # channel user was kicked from
535 fields = ' channel=' << channel.to_s
539 def initialize(bot, server, source, target, channel, message="")
540 super(bot, server, source, target, message)
545 # class to manage IRC INVITEs
546 # +address?+ can be used as a shortcut to see if the bot was invited,
547 # which should be true except for server bugs
548 class InviteMessage < BasicUserMessage
549 # channel user was invited to
553 fields = ' channel=' << channel.to_s
557 def initialize(bot, server, source, target, channel, message="")
558 super(bot, server, source, target, message)
563 # class to pass IRC Nick changes in. @message contains the old nickame,
564 # @sourcenick contains the new one.
565 class NickMessage < BasicUserMessage
567 def initialize(bot, server, source, oldnick, newnick)
568 super(bot, server, source, oldnick, newnick)
569 @address = (source == @bot.myself)
582 fields = ' old=' << oldnick
583 fields << ' new=' << newnick
588 # class to manage mode changes
589 class ModeChangeMessage < BasicUserMessage
591 def initialize(bot, server, source, target, message="")
592 super(bot, server, source, target, message)
593 @address = (source == @bot.myself)
598 fields = ' modes=' << modes.inspect
603 # class to manage WHOIS replies
604 class WhoisMessage < BasicUserMessage
606 def initialize(bot, server, source, target, whois)
607 super(bot, server, source, target, "")
608 @address = (target == @bot.myself)
613 fields = ' whois=' << whois.inspect
618 # class to manage LIST replies
619 class ListMessage < BasicUserMessage
621 def initialize(bot, server, source, target, list=Hash.new)
622 super(bot, server, source, target, "")
627 fields = ' list=' << list.inspect
633 # class to manage NAME replies
634 class NamesMessage < BasicUserMessage
636 def initialize(bot, server, source, target, message="")
637 super(bot, server, source, target, message)
642 fields = ' users=' << users.inspect
647 # class to manager Ban list replies
648 class BanlistMessage < BasicUserMessage
652 def initialize(bot, server, source, target, message="")
653 super(bot, server, source, target, message)
658 fields = ' bans=' << bans.inspect
663 class QuitMessage < BasicUserMessage
664 attr_accessor :was_on
665 def initialize(bot, server, source, target, message="")
666 super(bot, server, source, target, message)
671 class TopicMessage < BasicUserMessage
674 # topic set at (unixtime)
675 attr_reader :timestamp
676 # topic set on channel
679 # :info if topic info, :set if topic set
680 attr_accessor :info_or_set
681 def initialize(bot, server, source, channel, topic=ChannelTopic.new)
682 super(bot, server, source, channel, topic.text)
684 @timestamp = topic.set_on
690 fields = ' topic=' << topic
691 fields << ' (set on ' << timestamp << ')'
696 # class to manage channel joins
697 class JoinMessage < BasicUserMessage
702 fields = ' channel=' << channel.to_s
706 def initialize(bot, server, source, channel, message="")
707 super(bot, server, source, channel, message)
709 # in this case sourcenick is the nick that could be the bot
710 @address = (source == @bot.myself)
714 # class to manage channel parts
715 # same as a join, but can have a message too
716 class PartMessage < JoinMessage
719 # class to handle ERR_NOSUCHNICK and ERR_NOSUCHCHANNEL
720 class NoSuchTargetMessage < BasicUserMessage
721 # the channel or nick that was not found
724 def initialize(bot, server, source, target, message='')
725 super(bot, server, source, target, message)
731 class UnknownMessage < BasicUserMessage