4 # :title: IRC message datastructures
7 BotConfig.register BotConfigArrayValue.new('core.address_prefix',
8 :default => [], :wizard => true,
9 :desc => "what non nick-matching prefixes should the bot respond to as if addressed (e.g !, so that '!foo' is treated like 'rbot: foo')"
12 BotConfig.register BotConfigBooleanValue.new('core.reply_with_nick',
13 :default => false, :wizard => true,
14 :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!')"
17 BotConfig.register BotConfigStringValue.new('core.nick_postfix',
18 :default => ':', :wizard => true,
19 :desc => "when replying with nick put this character after the nick of the user the bot is replying to"
22 # Define standard IRC attriubtes (not so standard actually,
23 # but the closest thing we have ...)
30 # Color is prefixed by \003 and followed by optional
31 # foreground and background specifications, two-digits-max
32 # numbers separated by a comma. One of the two parts
35 ColorRx = /#{Color}\d?\d?(?:,\d\d?)?/
37 # Standard color codes
64 # Convert a String or Symbol into a color number
65 def Irc.find_color(data)
69 f = if String === data
82 # Insert the full color code for a given
83 # foreground/background combination.
84 def Irc.color(fg=nil,bg=nil)
87 str << Irc.find_color(fg).to_s
90 str << "," << Irc.find_color(bg).to_s
95 # base user message class, all user messages derive from this
96 # (a user message is defined as having a source hostmask, a target
97 # nick/channel and a message part)
98 class BasicUserMessage
106 # when the message was received
109 # User that originated the message
112 # User/Channel message was sent to
115 # contents of the message
116 attr_accessor :message
118 # has the message been replied to/handled by a plugin?
119 attr_accessor :replied
121 # instantiate a new Message
122 # bot:: associated bot class
123 # server:: Server where the message took place
124 # source:: User that sent the message
125 # target:: User/Channel is destined for
126 # message:: actual message
127 def initialize(bot, server, source, target, message)
128 @msg_wants_id = false unless defined? @msg_wants_id
135 @message = BasicUserMessage.stripcolour message
140 if @msg_wants_id && @server.capabilities[:"identify-msg"]
141 if @message =~ /^([-+])(.*)/
142 @identified = ($1=="+")
145 warning "Message does not have identification"
149 if target && target == @bot.myself
155 # Access the nick of the source
158 @source.nick rescue @source.to_s
161 # Access the user@host of the source
164 "#{@source.user}@#{@source.host}" rescue @source.to_s
167 # Access the botuser corresponding to the source, if any
170 m.source.botuser rescue @bot.auth.everyone
174 # Was the message from an identified user?
179 # returns true if the message was addressed to the bot.
180 # This includes any private message to the bot, or any public message
181 # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
182 # a kick message when bot was kicked etc.
187 # has this message been replied to by a plugin?
192 # strip mIRC colour escapes from a string
193 def BasicUserMessage.stripcolour(string)
194 return "" unless string
195 ret = string.gsub(ColorRx, "")
196 #ret.tr!("\x00-\x1f", "")
202 # class for handling IRC user messages. Includes some utilities for handling
203 # the message, for example in plugins.
204 # The +message+ member will have any bot addressing "^bot: " removed
205 # (address? will return true in this case)
206 class UserMessage < BasicUserMessage
208 # for plugin messages, the name of the plugin invoked by the message
211 # for plugin messages, the rest of the message, with the plugin name
215 # convenience member. Who to reply to (i.e. would be sourcenick for a
216 # privately addressed message, or target (the channel) for a publicly
220 # channel the message was in, nil for privately addressed messages
223 # for PRIVMSGs, false unless the message was a CTCP command,
224 # in which case it evaluates to the CTCP command itself
225 # (TIME, PING, VERSION, etc). The CTCP command parameters
226 # are then stored in the message.
229 # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
230 # will be stripped from the message)
233 # instantiate a new UserMessage
234 # bot:: associated bot class
235 # source:: hostmask of the message source
236 # target:: nick/channel message is destined for
237 # message:: message part
238 def initialize(bot, server, source, target, message)
239 super(bot, server, source, target, message)
246 if target == @bot.myself
256 # check for option extra addressing prefixes, e.g "|search foo", or
257 # "!version" - first match wins
258 bot.config['core.address_prefix'].each {|mprefix|
259 if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
265 # even if they used above prefixes, we allow for silly people who
266 # combine all possible types, e.g. "|rbot: hello", or
267 # "/msg rbot rbot: hello", etc
268 if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
272 if(@message =~ /^\001(\S+)(\s(.+))?\001/)
274 # FIXME need to support quoting of NULL and CR/LF, see
275 # http://www.irchelp.org/irchelp/rfc/ctcpspec.html
276 @message = $3 || String.new
277 @action = @ctcp == 'ACTION'
278 debug "Received CTCP command #{@ctcp} with options #{@message} (action? #{@action})"
281 # free splitting for plugins
282 @params = @message.dup
283 if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
284 @plugin = $1.downcase
285 @params = nil unless @params.length > 0
289 # returns true for private messages, e.g. "/msg bot hello"
294 # returns true if the message was in a channel
303 # convenience method to reply to a message, useful in plugins. It's the
305 # <tt>@bot.say m.replyto, string</tt>
306 # So if the message is private, it will reply to the user. If it was
307 # in a channel, it will reply in the channel.
308 def plainreply(string, options={})
309 @bot.say @replyto, string, options
313 # Same as reply, but when replying in public it adds the nick of the user
314 # the bot is replying to
315 def nickreply(string, options={})
316 extra = self.public? ? "#{@source}#{@bot.config['core.nick_postfix']} " : ""
317 @bot.say @replyto, extra + string, options
321 # the default reply style is to nickreply unless the reply already contains
322 # the nick or core.reply_with_nick is set to false
324 def reply(string, options={})
325 if @bot.config['core.reply_with_nick'] and not string =~ /\b#{@source}\b/
326 return nickreply(string, options)
328 plainreply(string, options)
331 # convenience method to reply to a message with an action. It's the
333 # <tt>@bot.action m.replyto, string</tt>
334 # So if the message is private, it will reply to the user. If it was
335 # in a channel, it will reply in the channel.
336 def act(string, options={})
337 @bot.action @replyto, string, options
341 # send a CTCP response, i.e. a private NOTICE to the sender
342 # with the same CTCP command and the reply as a parameter
343 def ctcp_reply(string, options={})
344 @bot.ctcp_notice @source, @ctcp, string, options
347 # convenience method to reply "okay" in the current language to the
350 self.plainreply @bot.lang.get("okay")
353 # Like the above, but append the username
355 str = @bot.lang.get("okay").dup
357 # remove final punctuation
358 str.gsub!(/[!,.]$/,"")
359 str += ", #{@source}"
364 # the default okay style is the same as the default reply style
367 if @bot.config['core.reply_with_nick']
375 # class to manage IRC PRIVMSGs
376 class PrivMessage < UserMessage
377 def initialize(bot, server, source, target, message)
383 # class to manage IRC NOTICEs
384 class NoticeMessage < UserMessage
385 def initialize(bot, server, source, target, message)
391 # class to manage IRC KICKs
392 # +address?+ can be used as a shortcut to see if the bot was kicked,
393 # basically, +target+ was kicked from +channel+ by +source+ with +message+
394 class KickMessage < BasicUserMessage
395 # channel user was kicked from
398 def initialize(bot, server, source, target, channel, message="")
399 super(bot, server, source, target, message)
404 # class to pass IRC Nick changes in. @message contains the old nickame,
405 # @sourcenick contains the new one.
406 class NickMessage < BasicUserMessage
407 def initialize(bot, server, source, oldnick, newnick)
408 super(bot, server, source, oldnick, newnick)
420 class QuitMessage < BasicUserMessage
421 def initialize(bot, server, source, target, message="")
422 super(bot, server, source, target, message)
426 class TopicMessage < BasicUserMessage
429 # topic set at (unixtime)
430 attr_reader :timestamp
431 # topic set on channel
434 def initialize(bot, server, source, channel, topic=ChannelTopic.new)
435 super(bot, server, source, channel, topic.text)
437 @timestamp = topic.set_on
442 # class to manage channel joins
443 class JoinMessage < BasicUserMessage
446 def initialize(bot, server, source, channel, message="")
447 super(bot, server, source, channel, message)
449 # in this case sourcenick is the nick that could be the bot
450 @address = (source == @bot.myself)
454 # class to manage channel parts
455 # same as a join, but can have a message too
456 class PartMessage < JoinMessage