]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/message.rb
remove whitespace
[user/henk/code/ruby/rbot.git] / lib / rbot / message.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: IRC message datastructures
5
6 module Irc
7
8
9   class Bot
10     module Config
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')"
14       )
15
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!')"
19       )
20
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"
24       )
25       Config.register BooleanValue.new('core.private_replies',
26         :default => false,
27         :desc => 'Should the bot reply to private instead of the channel?'
28       )
29     end
30   end
31
32
33   # Define standard IRC attriubtes (not so standard actually,
34   # but the closest thing we have ...)
35   Bold = "\002"
36   Underline = "\037"
37   Reverse = "\026"
38   Italic = "\011"
39   NormalText = "\017"
40   AttributeRx = /#{Bold}|#{Underline}|#{Reverse}|#{Italic}|#{NormalText}/
41
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
45   # must be present.
46   Color = "\003"
47   ColorRx = /#{Color}\d?\d?(?:,\d\d?)?/
48
49   FormattingRx = /#{AttributeRx}|#{ColorRx}/
50
51   # Standard color codes
52   ColorCode = {
53     :black      => 1,
54     :blue       => 2,
55     :navyblue   => 2,
56     :navy_blue  => 2,
57     :green      => 3,
58     :red        => 4,
59     :brown      => 5,
60     :purple     => 6,
61     :olive      => 7,
62     :yellow     => 8,
63     :limegreen  => 9,
64     :lime_green => 9,
65     :teal       => 10,
66     :aqualight  => 11,
67     :aqua_light => 11,
68     :royal_blue => 12,
69     :hotpink    => 13,
70     :hot_pink   => 13,
71     :darkgray   => 14,
72     :dark_gray  => 14,
73     :lightgray  => 15,
74     :light_gray => 15,
75     :white      => 16
76   }
77
78   # Convert a String or Symbol into a color number
79   def Irc.find_color(data)
80     "%02d" % if Integer === data
81       data
82     else
83       f = if String === data
84             data.intern
85           else
86             data
87           end
88       if ColorCode.key?(f)
89         ColorCode[f]
90       else
91         0
92       end
93     end
94   end
95
96   # Insert the full color code for a given
97   # foreground/background combination.
98   def Irc.color(fg=nil,bg=nil)
99     str = Color.dup
100     if fg
101      str << Irc.find_color(fg)
102     end
103     if bg
104       str << "," << Irc.find_color(bg)
105     end
106     return str
107   end
108
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
113
114     # associated bot
115     attr_reader :bot
116
117     # associated server
118     attr_reader :server
119
120     # when the message was received
121     attr_reader :time
122
123     # User that originated the message
124     attr_reader :source
125
126     # User/Channel message was sent to
127     attr_reader :target
128
129     # contents of the message (stripped of initial/final format codes)
130     attr_accessor :message
131
132     # contents of the message (for logging purposes)
133     attr_accessor :logmessage
134
135     # contents of the message (stripped of all formatting)
136     attr_accessor :plainmessage
137
138     # has the message been replied to/handled by a plugin?
139     attr_accessor :replied
140     alias :replied? :replied
141
142     # should the message be ignored?
143     attr_accessor :ignored
144     alias :ignored? :ignored
145
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
149
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?
162       ret << ' (addressed to me)' if address?
163       ret << ' (replied)' if replied?
164       ret << ' (ignored)' if ignored?
165       ret << ' (in thread)' if in_thread?
166       ret << '>'
167     end
168
169     # instantiate a new Message
170     # bot::      associated bot class
171     # server::   Server where the message took place
172     # source::   User that sent the message
173     # target::   User/Channel is destined for
174     # message::  actual message
175     def initialize(bot, server, source, target, message)
176       @msg_wants_id = false unless defined? @msg_wants_id
177
178       @time = Time.now
179       @bot = bot
180       @source = source
181       @address = false
182       @target = target
183       @message = message || ""
184       @replied = false
185       @server = server
186       @ignored = false
187       @in_thread = false
188
189       @identified = false
190       if @msg_wants_id && @server.capabilities[:"identify-msg"]
191         if @message =~ /^([-+])(.*)/
192           @identified = ($1=="+")
193           @message = $2
194         else
195           warning "Message does not have identification"
196         end
197       end
198       @logmessage = @message.dup
199       @plainmessage = BasicUserMessage.strip_formatting(@message)
200       @message = BasicUserMessage.strip_initial_formatting(@message)
201
202       if target && target == @bot.myself
203         @address = true
204       end
205
206     end
207
208     # Access the nick of the source
209     #
210     def sourcenick
211       @source.nick rescue @source.to_s
212     end
213
214     # Access the user@host of the source
215     #
216     def sourceaddress
217       "#{@source.user}@#{@source.host}" rescue @source.to_s
218     end
219
220     # Access the botuser corresponding to the source, if any
221     #
222     def botuser
223       source.botuser rescue @bot.auth.everyone
224     end
225
226
227     # Was the message from an identified user?
228     def identified?
229       return @identified
230     end
231
232     # returns true if the message was addressed to the bot.
233     # This includes any private message to the bot, or any public message
234     # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
235     # a kick message when bot was kicked etc.
236     def address?
237       return @address
238     end
239
240     # strip mIRC colour escapes from a string
241     def BasicUserMessage.stripcolour(string)
242       return "" unless string
243       ret = string.gsub(ColorRx, "")
244       #ret.tr!("\x00-\x1f", "")
245       ret
246     end
247
248     def BasicUserMessage.strip_initial_formatting(string)
249       return "" unless string
250       ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
251     end
252
253     def BasicUserMessage.strip_formatting(string)
254       string.gsub(FormattingRx,"")
255     end
256
257   end
258
259   # class for handling welcome messages from the server
260   class WelcomeMessage < BasicUserMessage
261   end
262
263   # class for handling MOTD from the server. Yes, MotdMessage
264   # is somewhat redundant, but it fits with the naming scheme
265   class MotdMessage < BasicUserMessage
266   end
267
268   # class for handling IRC user messages. Includes some utilities for handling
269   # the message, for example in plugins.
270   # The +message+ member will have any bot addressing "^bot: " removed
271   # (address? will return true in this case)
272   class UserMessage < BasicUserMessage
273
274     def inspect
275       fields = ' plugin=' << plugin.inspect
276       fields << ' params=' << params.inspect
277       fields << ' channel=' << channel.to_s if channel
278       fields << ' (reply to ' << replyto.to_s << ')'
279       if self.private?
280         fields << ' (private)'
281       else
282         fields << ' (public)'
283       end
284       if self.action?
285         fields << ' (action)'
286       elsif ctcp
287         fields << ' (CTCP ' << ctcp << ')'
288       end
289       super(fields)
290     end
291
292     # for plugin messages, the name of the plugin invoked by the message
293     attr_reader :plugin
294
295     # for plugin messages, the rest of the message, with the plugin name
296     # removed
297     attr_reader :params
298
299     # convenience member. Who to reply to (i.e. would be sourcenick for a
300     # privately addressed message, or target (the channel) for a publicly
301     # addressed message
302     attr_reader :replyto
303
304     # channel the message was in, nil for privately addressed messages
305     attr_reader :channel
306
307     # for PRIVMSGs, false unless the message was a CTCP command,
308     # in which case it evaluates to the CTCP command itself
309     # (TIME, PING, VERSION, etc). The CTCP command parameters
310     # are then stored in the message.
311     attr_reader :ctcp
312
313     # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
314     # will be stripped from the message)
315     attr_reader :action
316
317     # instantiate a new UserMessage
318     # bot::      associated bot class
319     # source::   hostmask of the message source
320     # target::   nick/channel message is destined for
321     # message::  message part
322     def initialize(bot, server, source, target, message)
323       super(bot, server, source, target, message)
324       @target = target
325       @private = false
326       @plugin = nil
327       @ctcp = false
328       @action = false
329
330       if target == @bot.myself
331         @private = true
332         @address = true
333         @channel = nil
334         @replyto = source
335       else
336         @replyto = @target
337         @channel = @target
338       end
339
340       # check for option extra addressing prefixes, e.g "|search foo", or
341       # "!version" - first match wins
342       bot.config['core.address_prefix'].each {|mprefix|
343         if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
344           @address = true
345           break
346         end
347       }
348
349       # even if they used above prefixes, we allow for silly people who
350       # combine all possible types, e.g. "|rbot: hello", or
351       # "/msg rbot rbot: hello", etc
352       if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
353         @address = true
354       end
355
356       if(@message =~ /^\001(\S+)(\s(.+))?\001/)
357         @ctcp = $1
358         # FIXME need to support quoting of NULL and CR/LF, see
359         # http://www.irchelp.org/irchelp/rfc/ctcpspec.html
360         @message = $3 || String.new
361         @action = @ctcp == 'ACTION'
362         debug "Received CTCP command #{@ctcp} with options #{@message} (action? #{@action})"
363         @logmessage = @message.dup
364         @plainmessage = BasicUserMessage.strip_formatting(@message)
365         @message = BasicUserMessage.strip_initial_formatting(@message)
366       end
367
368       # free splitting for plugins
369       @params = @message.dup
370       # Created messges (such as by fake_message) can contain multiple lines
371       if @params.gsub!(/\A\s*(\S+)[\s$]*/m, "")
372         @plugin = $1.downcase
373         @params = nil unless @params.length > 0
374       end
375     end
376
377     # returns true for private messages, e.g. "/msg bot hello"
378     def private?
379       return @private
380     end
381
382     # returns true if the message was in a channel
383     def public?
384       return !@private
385     end
386
387     def action?
388       return @action
389     end
390
391     # convenience method to reply to a message, useful in plugins. It's the
392     # same as doing:
393     # <tt>@bot.say m.replyto, string</tt>
394     # So if the message is private, it will reply to the user. If it was
395     # in a channel, it will reply in the channel.
396     def plainreply(string, options={})
397       reply string, {:nick => false}.merge(options)
398     end
399
400     # Same as reply, but when replying in public it adds the nick of the user
401     # the bot is replying to
402     def nickreply(string, options={})
403       reply string, {:nick => true}.merge(options)
404     end
405
406     # The general way to reply to a command. The following options are available:
407     # :nick [false, true, :auto]
408     #   state if the nick of the user calling the command should be prepended
409     #   :auto uses core.reply_with_nick
410     #
411     # :to [:private, :public, :auto]
412     #   where should the bot reply?
413     #   :private always reply to the nick
414     #   :public reply to the channel (if available)
415     #   :auto uses core.private_replies
416
417     def reply(string, options={})
418       opts = {:nick => :auto, :to => :auto}.merge options
419
420       if opts[:nick] == :auto
421         opts[:nick] = @bot.config['core.reply_with_nick']
422       end
423
424       if !self.public?
425         opts[:to] = :private
426       elsif opts[:to] == :auto
427         opts[:to] = @bot.config['core.private_replies'] ? :private : :public
428       end
429
430       if (opts[:nick] &&
431           opts[:to] != :private &&
432           string !~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/)
433         string = "#{@source}#{@bot.config['core.nick_postfix']} #{string}"
434       end
435       to = (opts[:to] == :private) ? source : @channel
436       @bot.say to, string, options
437       @replied = true
438     end
439
440     # convenience method to reply to a message with an action. It's the
441     # same as doing:
442     # <tt>@bot.action m.replyto, string</tt>
443     # So if the message is private, it will reply to the user. If it was
444     # in a channel, it will reply in the channel.
445     def act(string, options={})
446       @bot.action @replyto, string, options
447       @replied = true
448     end
449
450     # send a CTCP response, i.e. a private NOTICE to the sender
451     # with the same CTCP command and the reply as a parameter
452     def ctcp_reply(string, options={})
453       @bot.ctcp_notice @source, @ctcp, string, options
454     end
455
456     # convenience method to reply "okay" in the current language to the
457     # message
458     def plainokay
459       self.reply @bot.lang.get("okay"), :nick => false
460     end
461
462     # Like the above, but append the username
463     def nickokay
464       str = @bot.lang.get("okay").dup
465       if self.public?
466         # remove final punctuation
467         str.gsub!(/[!,.]$/,"")
468         str += ", #{@source}"
469       end
470       self.reply str, :nick => false
471     end
472
473     # the default okay style is the same as the default reply style
474     #
475     def okay
476       @bot.config['core.reply_with_nick'] ? nickokay : plainokay
477     end
478
479     # send a NOTICE to the message source
480     #
481     def notify(msg,opts={})
482       @bot.notice(sourcenick, msg, opts)
483     end
484
485   end
486
487   # class to manage IRC PRIVMSGs
488   class PrivMessage < UserMessage
489     def initialize(bot, server, source, target, message, opts={})
490       @msg_wants_id = opts[:handle_id]
491       super(bot, server, source, target, message)
492     end
493   end
494
495   # class to manage IRC NOTICEs
496   class NoticeMessage < UserMessage
497     def initialize(bot, server, source, target, message, opts={})
498       @msg_wants_id = opts[:handle_id]
499       super(bot, server, source, target, message)
500     end
501   end
502
503   # class to manage IRC KICKs
504   # +address?+ can be used as a shortcut to see if the bot was kicked,
505   # basically, +target+ was kicked from +channel+ by +source+ with +message+
506   class KickMessage < BasicUserMessage
507     # channel user was kicked from
508     attr_reader :channel
509
510     def inspect
511       fields = ' channel=' << channel.to_s
512       super(fields)
513     end
514
515     def initialize(bot, server, source, target, channel, message="")
516       super(bot, server, source, target, message)
517       @channel = channel
518     end
519   end
520
521   # class to manage IRC INVITEs
522   # +address?+ can be used as a shortcut to see if the bot was invited,
523   # which should be true except for server bugs
524   class InviteMessage < BasicUserMessage
525     # channel user was invited to
526     attr_reader :channel
527
528     def inspect
529       fields = ' channel=' << channel.to_s
530       super(fields)
531     end
532
533     def initialize(bot, server, source, target, channel, message="")
534       super(bot, server, source, target, message)
535       @channel = channel
536     end
537   end
538
539   # class to pass IRC Nick changes in. @message contains the old nickame,
540   # @sourcenick contains the new one.
541   class NickMessage < BasicUserMessage
542     attr_accessor :is_on
543     def initialize(bot, server, source, oldnick, newnick)
544       super(bot, server, source, oldnick, newnick)
545       @address = (source == @bot.myself)
546       @is_on = []
547     end
548
549     def oldnick
550       return @target
551     end
552
553     def newnick
554       return @message
555     end
556
557     def inspect
558       fields = ' old=' << oldnick
559       fields << ' new=' << newnick
560       super(fields)
561     end
562   end
563
564   # class to manage mode changes
565   class ModeChangeMessage < BasicUserMessage
566     attr_accessor :modes
567     def initialize(bot, server, source, target, message="")
568       super(bot, server, source, target, message)
569       @address = (source == @bot.myself)
570       @modes = []
571     end
572
573     def inspect
574       fields = ' modes=' << modes.inspect
575       super(fields)
576     end
577   end
578
579   # class to manage WHOIS replies
580   class WhoisMessage < BasicUserMessage
581     attr_reader :whois
582     def initialize(bot, server, source, target, whois)
583       super(bot, server, source, target, "")
584       @address = (target == @bot.myself)
585       @whois = whois
586     end
587
588     def inspect
589       fields = ' whois=' << whois.inspect
590       super(fields)
591     end
592   end
593
594   # class to manage NAME replies
595   class NamesMessage < BasicUserMessage
596     attr_accessor :users
597     def initialize(bot, server, source, target, message="")
598       super(bot, server, source, target, message)
599       @users = []
600     end
601
602     def inspect
603       fields = ' users=' << users.inspect
604       super(fields)
605     end
606   end
607
608   class QuitMessage < BasicUserMessage
609     attr_accessor :was_on
610     def initialize(bot, server, source, target, message="")
611       super(bot, server, source, target, message)
612       @was_on = []
613     end
614   end
615
616   class TopicMessage < BasicUserMessage
617     # channel topic
618     attr_reader :topic
619     # topic set at (unixtime)
620     attr_reader :timestamp
621     # topic set on channel
622     attr_reader :channel
623
624     # :info if topic info, :set if topic set
625     attr_accessor :info_or_set
626     def initialize(bot, server, source, channel, topic=ChannelTopic.new)
627       super(bot, server, source, channel, topic.text)
628       @topic = topic
629       @timestamp = topic.set_on
630       @channel = channel
631       @info_or_set = nil
632     end
633
634     def inspect
635       fields = ' topic=' << topic
636       fields << ' (set on ' << timestamp << ')'
637       super(fields)
638     end
639   end
640
641   # class to manage channel joins
642   class JoinMessage < BasicUserMessage
643     # channel joined
644     attr_reader :channel
645
646     def inspect
647       fields = ' channel=' << channel.to_s
648       super(fields)
649     end
650
651     def initialize(bot, server, source, channel, message="")
652       super(bot, server, source, channel, message)
653       @channel = channel
654       # in this case sourcenick is the nick that could be the bot
655       @address = (source == @bot.myself)
656     end
657   end
658
659   # class to manage channel parts
660   # same as a join, but can have a message too
661   class PartMessage < JoinMessage
662   end
663
664   class UnknownMessage < BasicUserMessage
665   end
666 end