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"
29 # Define standard IRC attriubtes (not so standard actually,
30 # but the closest thing we have ...)
36 AttributeRx = /#{Bold}|#{Underline}|#{Reverse}|#{Italic}|#{NormalText}/
38 # Color is prefixed by \003 and followed by optional
39 # foreground and background specifications, two-digits-max
40 # numbers separated by a comma. One of the two parts
43 ColorRx = /#{Color}\d?\d?(?:,\d\d?)?/
45 FormattingRx = /#{AttributeRx}|#{ColorRx}/
47 # Standard color codes
74 # Convert a String or Symbol into a color number
75 def Irc.find_color(data)
76 "%02d" % if Integer === data
79 f = if String === data
92 # Insert the full color code for a given
93 # foreground/background combination.
94 def Irc.color(fg=nil,bg=nil)
97 str << Irc.find_color(fg)
100 str << "," << Irc.find_color(bg)
105 # base user message class, all user messages derive from this
106 # (a user message is defined as having a source hostmask, a target
107 # nick/channel and a message part)
108 class BasicUserMessage
116 # when the message was received
119 # User that originated the message
122 # User/Channel message was sent to
125 # contents of the message (stripped of initial/final format codes)
126 attr_accessor :message
128 # contents of the message (for logging purposes)
129 attr_accessor :logmessage
131 # contents of the message (stripped of all formatting)
132 attr_accessor :plainmessage
134 # has the message been replied to/handled by a plugin?
135 attr_accessor :replied
136 alias :replied? :replied
138 # should the message be ignored?
139 attr_accessor :ignored
140 alias :ignored? :ignored
142 # set this to true if the method that delegates the message is run in a thread
143 attr_accessor :in_thread
144 alias :in_thread? :in_thread
146 def inspect(fields=nil)
147 ret = self.__to_s__[0..-2]
148 ret << ' bot=' << @bot.__to_s__
149 ret << ' server=' << server.to_s
150 ret << ' time=' << time.to_s
151 ret << ' source=' << source.to_s
152 ret << ' target=' << target.to_s
153 ret << ' message=' << message.inspect
154 ret << ' logmessage=' << logmessage.inspect
155 ret << ' plainmessage=' << plainmessage.inspect
156 ret << fields if fields
157 ret << ' (identified)' if identified?
158 ret << ' (addressed to me)' if address?
159 ret << ' (replied)' if replied?
160 ret << ' (ignored)' if ignored?
161 ret << ' (in thread)' if in_thread?
165 # instantiate a new Message
166 # bot:: associated bot class
167 # server:: Server where the message took place
168 # source:: User that sent the message
169 # target:: User/Channel is destined for
170 # message:: actual message
171 def initialize(bot, server, source, target, message)
172 @msg_wants_id = false unless defined? @msg_wants_id
179 @message = message || ""
186 if @msg_wants_id && @server.capabilities[:"identify-msg"]
187 if @message =~ /^([-+])(.*)/
188 @identified = ($1=="+")
191 warning "Message does not have identification"
194 @logmessage = @message.dup
195 @plainmessage = BasicUserMessage.strip_formatting(@message)
196 @message = BasicUserMessage.strip_initial_formatting(@message)
198 if target && target == @bot.myself
204 # Access the nick of the source
207 @source.nick rescue @source.to_s
210 # Access the user@host of the source
213 "#{@source.user}@#{@source.host}" rescue @source.to_s
216 # Access the botuser corresponding to the source, if any
219 source.botuser rescue @bot.auth.everyone
223 # Was the message from an identified user?
228 # returns true if the message was addressed to the bot.
229 # This includes any private message to the bot, or any public message
230 # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
231 # a kick message when bot was kicked etc.
236 # strip mIRC colour escapes from a string
237 def BasicUserMessage.stripcolour(string)
238 return "" unless string
239 ret = string.gsub(ColorRx, "")
240 #ret.tr!("\x00-\x1f", "")
244 def BasicUserMessage.strip_initial_formatting(string)
245 return "" unless string
246 ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
249 def BasicUserMessage.strip_formatting(string)
250 string.gsub(FormattingRx,"")
255 # class for handling welcome messages from the server
256 class WelcomeMessage < BasicUserMessage
259 # class for handling MOTD from the server. Yes, MotdMessage
260 # is somewhat redundant, but it fits with the naming scheme
261 class MotdMessage < BasicUserMessage
264 # class for handling IRC user messages. Includes some utilities for handling
265 # the message, for example in plugins.
266 # The +message+ member will have any bot addressing "^bot: " removed
267 # (address? will return true in this case)
268 class UserMessage < BasicUserMessage
271 fields = ' plugin=' << plugin.inspect
272 fields << ' params=' << params.inspect
273 fields << ' channel=' << channel.to_s if channel
274 fields << ' (reply to ' << replyto.to_s << ')'
276 fields << ' (private)'
278 fields << ' (public)'
281 fields << ' (action)'
283 fields << ' (CTCP ' << ctcp << ')'
288 # for plugin messages, the name of the plugin invoked by the message
291 # for plugin messages, the rest of the message, with the plugin name
295 # convenience member. Who to reply to (i.e. would be sourcenick for a
296 # privately addressed message, or target (the channel) for a publicly
300 # channel the message was in, nil for privately addressed messages
303 # for PRIVMSGs, false unless the message was a CTCP command,
304 # in which case it evaluates to the CTCP command itself
305 # (TIME, PING, VERSION, etc). The CTCP command parameters
306 # are then stored in the message.
309 # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
310 # will be stripped from the message)
313 # instantiate a new UserMessage
314 # bot:: associated bot class
315 # source:: hostmask of the message source
316 # target:: nick/channel message is destined for
317 # message:: message part
318 def initialize(bot, server, source, target, message)
319 super(bot, server, source, target, message)
326 if target == @bot.myself
336 # check for option extra addressing prefixes, e.g "|search foo", or
337 # "!version" - first match wins
338 bot.config['core.address_prefix'].each {|mprefix|
339 if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
345 # even if they used above prefixes, we allow for silly people who
346 # combine all possible types, e.g. "|rbot: hello", or
347 # "/msg rbot rbot: hello", etc
348 if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
352 if(@message =~ /^\001(\S+)(\s(.+))?\001/)
354 # FIXME need to support quoting of NULL and CR/LF, see
355 # http://www.irchelp.org/irchelp/rfc/ctcpspec.html
356 @message = $3 || String.new
357 @action = @ctcp == 'ACTION'
358 debug "Received CTCP command #{@ctcp} with options #{@message} (action? #{@action})"
359 @logmessage = @message.dup
360 @plainmessage = BasicUserMessage.strip_formatting(@message)
361 @message = BasicUserMessage.strip_initial_formatting(@message)
364 # free splitting for plugins
365 @params = @message.dup
366 # Created messges (such as by fake_message) can contain multiple lines
367 if @params.gsub!(/\A\s*(\S+)[\s$]*/m, "")
368 @plugin = $1.downcase
369 @params = nil unless @params.length > 0
373 # returns true for private messages, e.g. "/msg bot hello"
378 # returns true if the message was in a channel
387 # convenience method to reply to a message, useful in plugins. It's the
389 # <tt>@bot.say m.replyto, string</tt>
390 # So if the message is private, it will reply to the user. If it was
391 # in a channel, it will reply in the channel.
392 def plainreply(string, options={})
393 reply string, {:nick => false}.merge(options)
396 # Same as reply, but when replying in public it adds the nick of the user
397 # the bot is replying to
398 def nickreply(string, options={})
399 reply string, {:nick => true}.merge(options)
402 # The general way to reply to a command. The following options are available:
403 # :nick [false, true, :auto]
404 # state if the nick of the user calling the command should be prepended
405 # :auto uses core.reply_with_nick
407 def reply(string, options={})
408 opts = {:nick => :auto}.merge options
409 if opts[:nick] == :auto
410 opts[:nick] = @bot.config['core.reply_with_nick']
412 if (opts[:nick] && self.public? &&
413 string !~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/)
414 string = "#{@source}#{@bot.config['core.nick_postfix']} #{string}"
416 @bot.say @replyto, string, options
420 # convenience method to reply to a message with an action. It's the
422 # <tt>@bot.action m.replyto, string</tt>
423 # So if the message is private, it will reply to the user. If it was
424 # in a channel, it will reply in the channel.
425 def act(string, options={})
426 @bot.action @replyto, string, options
430 # send a CTCP response, i.e. a private NOTICE to the sender
431 # with the same CTCP command and the reply as a parameter
432 def ctcp_reply(string, options={})
433 @bot.ctcp_notice @source, @ctcp, string, options
436 # convenience method to reply "okay" in the current language to the
439 self.reply @bot.lang.get("okay"), :nick => false
442 # Like the above, but append the username
444 str = @bot.lang.get("okay").dup
446 # remove final punctuation
447 str.gsub!(/[!,.]$/,"")
448 str += ", #{@source}"
450 self.reply str, :nick => false
453 # the default okay style is the same as the default reply style
456 @bot.config['core.reply_with_nick'] ? nickokay : plainokay
459 # send a NOTICE to the message source
461 def notify(msg,opts={})
462 @bot.notice(sourcenick, msg, opts)
467 # class to manage IRC PRIVMSGs
468 class PrivMessage < UserMessage
469 def initialize(bot, server, source, target, message, opts={})
470 @msg_wants_id = opts[:handle_id]
471 super(bot, server, source, target, message)
475 # class to manage IRC NOTICEs
476 class NoticeMessage < UserMessage
477 def initialize(bot, server, source, target, message, opts={})
478 @msg_wants_id = opts[:handle_id]
479 super(bot, server, source, target, message)
483 # class to manage IRC KICKs
484 # +address?+ can be used as a shortcut to see if the bot was kicked,
485 # basically, +target+ was kicked from +channel+ by +source+ with +message+
486 class KickMessage < BasicUserMessage
487 # channel user was kicked from
491 fields = ' channel=' << channel.to_s
495 def initialize(bot, server, source, target, channel, message="")
496 super(bot, server, source, target, message)
501 # class to manage IRC INVITEs
502 # +address?+ can be used as a shortcut to see if the bot was invited,
503 # which should be true except for server bugs
504 class InviteMessage < BasicUserMessage
505 # channel user was invited to
509 fields = ' channel=' << channel.to_s
513 def initialize(bot, server, source, target, channel, message="")
514 super(bot, server, source, target, message)
519 # class to pass IRC Nick changes in. @message contains the old nickame,
520 # @sourcenick contains the new one.
521 class NickMessage < BasicUserMessage
523 def initialize(bot, server, source, oldnick, newnick)
524 super(bot, server, source, oldnick, newnick)
525 @address = (source == @bot.myself)
538 fields = ' old=' << oldnick
539 fields << ' new=' << newnick
544 # class to manage mode changes
545 class ModeChangeMessage < BasicUserMessage
547 def initialize(bot, server, source, target, message="")
548 super(bot, server, source, target, message)
549 @address = (source == @bot.myself)
554 fields = ' modes=' << modes.inspect
559 # class to manage WHOIS replies
560 class WhoisMessage < BasicUserMessage
562 def initialize(bot, server, source, target, whois)
563 super(bot, server, source, target, "")
564 @address = (target == @bot.myself)
569 fields = ' whois=' << whois.inspect
574 # class to manage NAME replies
575 class NamesMessage < BasicUserMessage
577 def initialize(bot, server, source, target, message="")
578 super(bot, server, source, target, message)
583 fields = ' users=' << users.inspect
588 class QuitMessage < BasicUserMessage
589 attr_accessor :was_on
590 def initialize(bot, server, source, target, message="")
591 super(bot, server, source, target, message)
596 class TopicMessage < BasicUserMessage
599 # topic set at (unixtime)
600 attr_reader :timestamp
601 # topic set on channel
604 # :info if topic info, :set if topic set
605 attr_accessor :info_or_set
606 def initialize(bot, server, source, channel, topic=ChannelTopic.new)
607 super(bot, server, source, channel, topic.text)
609 @timestamp = topic.set_on
615 fields = ' topic=' << topic
616 fields << ' (set on ' << timestamp << ')'
621 # class to manage channel joins
622 class JoinMessage < BasicUserMessage
627 fields = ' channel=' << channel.to_s
631 def initialize(bot, server, source, channel, message="")
632 super(bot, server, source, channel, message)
634 # in this case sourcenick is the nick that could be the bot
635 @address = (source == @bot.myself)
639 # class to manage channel parts
640 # same as a join, but can have a message too
641 class PartMessage < JoinMessage
644 class UnknownMessage < BasicUserMessage