X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Fmessage.rb;h=315088790bd90e26818a4c2b7eb027b456e9875d;hb=a6e1d47056be249989cbe40233422d4644d88739;hp=385f6c4cc8485fce0635e143c9a6f9d1014f9da7;hpb=88e0576dfa38586fb27dd52d29225b6de0af029a;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git
diff --git a/lib/rbot/message.rb b/lib/rbot/message.rb
index 385f6c4c..31508879 100644
--- a/lib/rbot/message.rb
+++ b/lib/rbot/message.rb
@@ -1,8 +1,110 @@
+#-- vim:sw=2:et
+#++
+#
+# :title: IRC message datastructures
+
module Irc
- BotConfig.register BotConfigArrayValue.new('core.address_prefix',
- :default => [], :wizard => true,
- :desc => "what non nick-matching prefixes should the bot respond to as if addressed (e.g !, so that '!foo' is treated like 'rbot: foo')"
- )
+
+
+ class Bot
+ module Config
+ Config.register ArrayValue.new('core.address_prefix',
+ :default => [], :wizard => true,
+ :desc => "what non nick-matching prefixes should the bot respond to as if addressed (e.g !, so that '!foo' is treated like 'rbot: foo')"
+ )
+
+ Config.register BooleanValue.new('core.reply_with_nick',
+ :default => false, :wizard => true,
+ :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!')"
+ )
+
+ Config.register StringValue.new('core.nick_postfix',
+ :default => ':', :wizard => true,
+ :desc => "when replying with nick put this character after the nick of the user the bot is replying to"
+ )
+ Config.register BooleanValue.new('core.private_replies',
+ :default => false,
+ :desc => 'Should the bot reply to private instead of the channel?'
+ )
+ end
+ end
+
+
+ # Define standard IRC attributes (not so standard actually,
+ # but the closest thing we have ...)
+ Bold = "\002"
+ Underline = "\037"
+ Reverse = "\026"
+ Italic = "\011"
+ NormalText = "\017"
+ AttributeRx = /#{Bold}|#{Underline}|#{Reverse}|#{Italic}|#{NormalText}/
+
+ # Color is prefixed by \003 and followed by optional
+ # foreground and background specifications, two-digits-max
+ # numbers separated by a comma. One of the two parts
+ # must be present.
+ Color = "\003"
+ ColorRx = /#{Color}\d?\d?(?:,\d\d?)?/
+
+ FormattingRx = /#{AttributeRx}|#{ColorRx}/
+
+ # Standard color codes
+ ColorCode = {
+ :black => 1,
+ :blue => 2,
+ :navyblue => 2,
+ :navy_blue => 2,
+ :green => 3,
+ :red => 4,
+ :brown => 5,
+ :purple => 6,
+ :olive => 7,
+ :yellow => 8,
+ :limegreen => 9,
+ :lime_green => 9,
+ :teal => 10,
+ :aqualight => 11,
+ :aqua_light => 11,
+ :royal_blue => 12,
+ :hotpink => 13,
+ :hot_pink => 13,
+ :darkgray => 14,
+ :dark_gray => 14,
+ :lightgray => 15,
+ :light_gray => 15,
+ :white => 0
+ }
+
+ # Convert a String or Symbol into a color number
+ def Irc.find_color(data)
+ "%02d" % if Integer === data
+ data
+ else
+ f = if String === data
+ data.intern
+ else
+ data
+ end
+ if ColorCode.key?(f)
+ ColorCode[f]
+ else
+ 0
+ end
+ end
+ end
+
+ # Insert the full color code for a given
+ # foreground/background combination.
+ def Irc.color(fg=nil,bg=nil)
+ str = Color.dup
+ if fg
+ str << Irc.find_color(fg)
+ end
+ if bg
+ str << "," << Irc.find_color(bg)
+ end
+ return str
+ end
# base user message class, all user messages derive from this
# (a user message is defined as having a source hostmask, a target
@@ -12,65 +114,122 @@ module Irc
# associated bot
attr_reader :bot
+ # associated server
+ attr_reader :server
+
# when the message was received
attr_reader :time
- # hostmask of message source
+ # User that originated the message
attr_reader :source
- # nick of message source
- attr_reader :sourcenick
-
- # url part of message source
- attr_reader :sourceaddress
-
- # nick/channel message was sent to
+ # User/Channel message was sent to
attr_reader :target
- # contents of the message
+ # contents of the message (stripped of initial/final format codes)
attr_accessor :message
+ # contents of the message (for logging purposes)
+ attr_accessor :logmessage
+
+ # contents of the message (stripped of all formatting)
+ attr_accessor :plainmessage
+
# has the message been replied to/handled by a plugin?
attr_accessor :replied
+ alias :replied? :replied
+
+ # should the message be ignored?
+ attr_accessor :ignored
+ alias :ignored? :ignored
+
+ # set this to true if the method that delegates the message is run in a thread
+ attr_accessor :in_thread
+ alias :in_thread? :in_thread
+
+ def inspect(fields=nil)
+ ret = self.__to_s__[0..-2]
+ ret << ' bot=' << @bot.__to_s__
+ ret << ' server=' << server.to_s
+ ret << ' time=' << time.to_s
+ ret << ' source=' << source.to_s
+ ret << ' target=' << target.to_s
+ ret << ' message=' << message.inspect
+ ret << ' logmessage=' << logmessage.inspect
+ ret << ' plainmessage=' << plainmessage.inspect
+ ret << fields if fields
+ ret << ' (identified)' if identified?
+ if address?
+ ret << ' (addressed to me'
+ ret << ', with prefix' if prefixed?
+ ret << ')'
+ end
+ ret << ' (replied)' if replied?
+ ret << ' (ignored)' if ignored?
+ ret << ' (in thread)' if in_thread?
+ ret << '>'
+ end
# instantiate a new Message
# bot:: associated bot class
- # source:: hostmask of the message source
- # target:: nick/channel message is destined for
- # message:: message part
- def initialize(bot, source, target, message)
+ # server:: Server where the message took place
+ # source:: User that sent the message
+ # target:: User/Channel is destined for
+ # message:: actual message
+ def initialize(bot, server, source, target, message)
@msg_wants_id = false unless defined? @msg_wants_id
@time = Time.now
@bot = bot
@source = source
@address = false
+ @prefixed = false
@target = target
- @message = BasicUserMessage.stripcolour message
+ @message = message || ""
@replied = false
+ @server = server
+ @ignored = false
+ @in_thread = false
@identified = false
- if @msg_wants_id && @bot.capabilities["identify-msg".to_sym]
- if @message =~ /([-+])(.*)/
+ if @msg_wants_id && @server.capabilities[:"identify-msg"]
+ if @message =~ /^([-+])(.*)/
@identified = ($1=="+")
@message = $2
else
warning "Message does not have identification"
end
end
+ @logmessage = @message.dup
+ @plainmessage = BasicUserMessage.strip_formatting(@message)
+ @message = BasicUserMessage.strip_initial_formatting(@message)
- # split source into consituent parts
- if source =~ /^((\S+)!(\S+))$/
- @sourcenick = $2
- @sourceaddress = $3
- end
-
- if target && target.downcase == @bot.nick.downcase
+ if target && target == @bot.myself
@address = true
end
end
+ # Access the nick of the source
+ #
+ def sourcenick
+ @source.nick rescue @source.to_s
+ end
+
+ # Access the user@host of the source
+ #
+ def sourceaddress
+ "#{@source.user}@#{@source.host}" rescue @source.to_s
+ end
+
+ # Access the botuser corresponding to the source, if any
+ #
+ def botuser
+ source.botuser rescue @bot.auth.everyone
+ end
+
+
+ # Was the message from an identified user?
def identified?
return @identified
end
@@ -83,19 +242,38 @@ module Irc
return @address
end
- # has this message been replied to by a plugin?
- def replied?
- return @replied
+ # returns true if the messaged was addressed to the bot via the address
+ # prefix. This can be used to tell appart "!do this" from "botname, do this"
+ def prefixed?
+ return @prefixed
end
# strip mIRC colour escapes from a string
def BasicUserMessage.stripcolour(string)
return "" unless string
- ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "")
+ ret = string.gsub(ColorRx, "")
#ret.tr!("\x00-\x1f", "")
ret
end
+ def BasicUserMessage.strip_initial_formatting(string)
+ return "" unless string
+ ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
+ end
+
+ def BasicUserMessage.strip_formatting(string)
+ string.gsub(FormattingRx,"")
+ end
+
+ end
+
+ # class for handling welcome messages from the server
+ class WelcomeMessage < BasicUserMessage
+ end
+
+ # class for handling MOTD from the server. Yes, MotdMessage
+ # is somewhat redundant, but it fits with the naming scheme
+ class MotdMessage < BasicUserMessage
end
# class for handling IRC user messages. Includes some utilities for handling
@@ -104,6 +282,24 @@ module Irc
# (address? will return true in this case)
class UserMessage < BasicUserMessage
+ def inspect
+ fields = ' plugin=' << plugin.inspect
+ fields << ' params=' << params.inspect
+ fields << ' channel=' << channel.to_s if channel
+ fields << ' (reply to ' << replyto.to_s << ')'
+ if self.private?
+ fields << ' (private)'
+ else
+ fields << ' (public)'
+ end
+ if self.action?
+ fields << ' (action)'
+ elsif ctcp
+ fields << ' (CTCP ' << ctcp << ')'
+ end
+ super(fields)
+ end
+
# for plugin messages, the name of the plugin invoked by the message
attr_reader :plugin
@@ -119,6 +315,12 @@ module Irc
# channel the message was in, nil for privately addressed messages
attr_reader :channel
+ # for PRIVMSGs, false unless the message was a CTCP command,
+ # in which case it evaluates to the CTCP command itself
+ # (TIME, PING, VERSION, etc). The CTCP command parameters
+ # are then stored in the message.
+ attr_reader :ctcp
+
# for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
# will be stripped from the message)
attr_reader :action
@@ -128,18 +330,19 @@ module Irc
# source:: hostmask of the message source
# target:: nick/channel message is destined for
# message:: message part
- def initialize(bot, source, target, message)
- super(bot, source, target, message)
+ def initialize(bot, server, source, target, message)
+ super(bot, server, source, target, message)
@target = target
@private = false
@plugin = nil
+ @ctcp = false
@action = false
- if target.downcase == @bot.nick.downcase
+ if target == @bot.myself
@private = true
@address = true
@channel = nil
- @replyto = @sourcenick
+ @replyto = source
else
@replyto = @target
@channel = @target
@@ -150,6 +353,7 @@ module Irc
bot.config['core.address_prefix'].each {|mprefix|
if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
@address = true
+ @prefixed = true
break
end
}
@@ -161,14 +365,22 @@ module Irc
@address = true
end
- if(@message =~ /^\001ACTION\s(.+)\001/)
- @message = $1
- @action = true
+ if(@message =~ /^\001(\S+)(\s(.+))?\001/)
+ @ctcp = $1
+ # FIXME need to support quoting of NULL and CR/LF, see
+ # http://www.irchelp.org/irchelp/rfc/ctcpspec.html
+ @message = $3 || String.new
+ @action = @ctcp == 'ACTION'
+ debug "Received CTCP command #{@ctcp} with options #{@message} (action? #{@action})"
+ @logmessage = @message.dup
+ @plainmessage = BasicUserMessage.strip_formatting(@message)
+ @message = BasicUserMessage.strip_initial_formatting(@message)
end
# free splitting for plugins
@params = @message.dup
- if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
+ # Created messges (such as by fake_message) can contain multiple lines
+ if @params.gsub!(/\A\s*(\S+)[\s$]*/m, "")
@plugin = $1.downcase
@params = nil unless @params.length > 0
end
@@ -193,32 +405,119 @@ module Irc
# @bot.say m.replyto, string
# So if the message is private, it will reply to the user. If it was
# in a channel, it will reply in the channel.
- def reply(string)
- @bot.say @replyto, string
+ def plainreply(string, options={})
+ reply string, {:nick => false}.merge(options)
+ end
+
+ # Same as reply, but when replying in public it adds the nick of the user
+ # the bot is replying to
+ def nickreply(string, options={})
+ reply string, {:nick => true}.merge(options)
+ end
+
+ # Same as nickreply, but always prepend the target's nick.
+ def nickreply!(string, options={})
+ reply string, {:nick => true, :forcenick => true}.merge(options)
+ end
+
+ # The general way to reply to a command. The following options are available:
+ # :nick [false, true, :auto]
+ # state if the nick of the user calling the command should be prepended
+ # :auto uses core.reply_with_nick
+ #
+ # :forcenick [false, true]
+ # if :nick is true, always prepend the target's nick, even if the nick
+ # already appears in the reply. Defaults to false.
+ #
+ # :to [:private, :public, :auto]
+ # where should the bot reply?
+ # :private always reply to the nick
+ # :public reply to the channel (if available)
+ # :auto uses core.private_replies
+ def reply(string, options={})
+ opts = {:nick => :auto, :forcenick => false, :to => :auto}.merge options
+
+ if opts[:nick] == :auto
+ opts[:nick] = @bot.config['core.reply_with_nick']
+ end
+
+ if !self.public?
+ opts[:to] = :private
+ elsif opts[:to] == :auto
+ opts[:to] = @bot.config['core.private_replies'] ? :private : :public
+ end
+
+ if (opts[:nick] &&
+ opts[:to] != :private &&
+ (string !~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/ ||
+ opts[:forcenick]))
+ string = "#{@source}#{@bot.config['core.nick_postfix']} #{string}"
+ end
+ to = (opts[:to] == :private) ? source : @channel
+ @bot.say to, string, options
+ @replied = true
+ end
+
+ # convenience method to reply to a message with an action. It's the
+ # same as doing:
+ # @bot.action m.replyto, string
+ # So if the message is private, it will reply to the user. If it was
+ # in a channel, it will reply in the channel.
+ def act(string, options={})
+ @bot.action @replyto, string, options
@replied = true
end
+ # send a CTCP response, i.e. a private NOTICE to the sender
+ # with the same CTCP command and the reply as a parameter
+ def ctcp_reply(string, options={})
+ @bot.ctcp_notice @source, @ctcp, string, options
+ end
+
# convenience method to reply "okay" in the current language to the
# message
+ def plainokay
+ self.reply @bot.lang.get("okay"), :nick => false
+ end
+
+ # Like the above, but append the username
+ def nickokay
+ str = @bot.lang.get("okay").dup
+ if self.public?
+ # remove final punctuation
+ str.gsub!(/[!,.]$/,"")
+ str += ", #{@source}"
+ end
+ self.reply str, :nick => false
+ end
+
+ # the default okay style is the same as the default reply style
+ #
def okay
- @bot.say @replyto, @bot.lang.get("okay")
+ @bot.config['core.reply_with_nick'] ? nickokay : plainokay
+ end
+
+ # send a NOTICE to the message source
+ #
+ def notify(msg,opts={})
+ @bot.notice(sourcenick, msg, opts)
end
end
# class to manage IRC PRIVMSGs
class PrivMessage < UserMessage
- def initialize(bot, source, target, message)
- @msg_wants_id = true
- super
+ def initialize(bot, server, source, target, message, opts={})
+ @msg_wants_id = opts[:handle_id]
+ super(bot, server, source, target, message)
end
end
# class to manage IRC NOTICEs
class NoticeMessage < UserMessage
- def initialize(bot, source, target, message)
- @msg_wants_id = true
- super
+ def initialize(bot, server, source, target, message, opts={})
+ @msg_wants_id = opts[:handle_id]
+ super(bot, server, source, target, message)
end
end
@@ -229,8 +528,31 @@ module Irc
# channel user was kicked from
attr_reader :channel
- def initialize(bot, source, target, channel, message="")
- super(bot, source, target, message)
+ def inspect
+ fields = ' channel=' << channel.to_s
+ super(fields)
+ end
+
+ def initialize(bot, server, source, target, channel, message="")
+ super(bot, server, source, target, message)
+ @channel = channel
+ end
+ end
+
+ # class to manage IRC INVITEs
+ # +address?+ can be used as a shortcut to see if the bot was invited,
+ # which should be true except for server bugs
+ class InviteMessage < BasicUserMessage
+ # channel user was invited to
+ attr_reader :channel
+
+ def inspect
+ fields = ' channel=' << channel.to_s
+ super(fields)
+ end
+
+ def initialize(bot, server, source, target, channel, message="")
+ super(bot, server, source, target, message)
@channel = channel
end
end
@@ -238,14 +560,93 @@ module Irc
# class to pass IRC Nick changes in. @message contains the old nickame,
# @sourcenick contains the new one.
class NickMessage < BasicUserMessage
- def initialize(bot, source, oldnick, newnick)
- super(bot, source, oldnick, newnick)
+ attr_accessor :is_on
+ def initialize(bot, server, source, oldnick, newnick)
+ super(bot, server, source, oldnick, newnick)
+ @address = (source == @bot.myself)
+ @is_on = []
+ end
+
+ def oldnick
+ return @target
+ end
+
+ def newnick
+ return @message
+ end
+
+ def inspect
+ fields = ' old=' << oldnick
+ fields << ' new=' << newnick
+ super(fields)
+ end
+ end
+
+ # class to manage mode changes
+ class ModeChangeMessage < BasicUserMessage
+ attr_accessor :modes
+ def initialize(bot, server, source, target, message="")
+ super(bot, server, source, target, message)
+ @address = (source == @bot.myself)
+ @modes = []
+ end
+
+ def inspect
+ fields = ' modes=' << modes.inspect
+ super(fields)
+ end
+ end
+
+ # class to manage WHOIS replies
+ class WhoisMessage < BasicUserMessage
+ attr_reader :whois
+ def initialize(bot, server, source, target, whois)
+ super(bot, server, source, target, "")
+ @address = (target == @bot.myself)
+ @whois = whois
+ end
+
+ def inspect
+ fields = ' whois=' << whois.inspect
+ super(fields)
+ end
+ end
+
+ # class to manage NAME replies
+ class NamesMessage < BasicUserMessage
+ attr_accessor :users
+ def initialize(bot, server, source, target, message="")
+ super(bot, server, source, target, message)
+ @users = []
+ end
+
+ def inspect
+ fields = ' users=' << users.inspect
+ super(fields)
+ end
+ end
+
+ # class to manager Ban list replies
+ class BanlistMessage < BasicUserMessage
+ # the bans
+ attr_accessor :bans
+
+ def initialize(bot, server, source, target, message="")
+ super(bot, server, source, target, message)
+ @bans = []
+ end
+
+ def inspect
+ fields = ' bans=' << bans.inspect
+ super(fields)
end
end
class QuitMessage < BasicUserMessage
- def initialize(bot, source, target, message="")
- super(bot, source, target, message)
+ attr_accessor :was_on
+ def initialize(bot, server, source, target, message="")
+ super(bot, server, source, target, message)
+ @was_on = []
end
end
@@ -257,11 +658,20 @@ module Irc
# topic set on channel
attr_reader :channel
- def initialize(bot, source, channel, timestamp, topic="")
- super(bot, source, channel, topic)
+ # :info if topic info, :set if topic set
+ attr_accessor :info_or_set
+ def initialize(bot, server, source, channel, topic=ChannelTopic.new)
+ super(bot, server, source, channel, topic.text)
@topic = topic
- @timestamp = timestamp
+ @timestamp = topic.set_on
@channel = channel
+ @info_or_set = nil
+ end
+
+ def inspect
+ fields = ' topic=' << topic
+ fields << ' (set on ' << timestamp << ')'
+ super(fields)
end
end
@@ -269,11 +679,17 @@ module Irc
class JoinMessage < BasicUserMessage
# channel joined
attr_reader :channel
- def initialize(bot, source, channel, message="")
- super(bot, source, channel, message)
+
+ def inspect
+ fields = ' channel=' << channel.to_s
+ super(fields)
+ end
+
+ def initialize(bot, server, source, channel, message="")
+ super(bot, server, source, channel, message)
@channel = channel
# in this case sourcenick is the nick that could be the bot
- @address = (sourcenick.downcase == @bot.nick.downcase)
+ @address = (source == @bot.myself)
end
end
@@ -281,4 +697,19 @@ module Irc
# same as a join, but can have a message too
class PartMessage < JoinMessage
end
+
+ # class to handle ERR_NOSUCHNICK and ERR_NOSUCHCHANNEL
+ class NoSuchTargetMessage < BasicUserMessage
+ # the channel or nick that was not found
+ attr_reader :target
+
+ def initialize(bot, server, source, target, message='')
+ super(bot, server, source, target, message)
+
+ @target = target
+ end
+ end
+
+ class UnknownMessage < BasicUserMessage
+ end
end