]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/message.rb
message: add #thanks method, similar to okay
[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 attributes (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      => 0
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       if address?
163         ret << ' (addressed to me'
164         ret << ', with prefix' if prefixed?
165         ret << ')'
166       end
167       ret << ' (replied)' if replied?
168       ret << ' (ignored)' if ignored?
169       ret << ' (in thread)' if in_thread?
170       ret << '>'
171     end
172
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
181
182       @time = Time.now
183       @bot = bot
184       @source = source
185       @address = false
186       @prefixed = false
187       @target = target
188       @message = message || ""
189       @replied = false
190       @server = server
191       @ignored = false
192       @in_thread = false
193
194       @identified = false
195       if @msg_wants_id && @server.capabilities[:"identify-msg"]
196         if @message =~ /^([-+])(.*)/
197           @identified = ($1=="+")
198           @message = $2
199         else
200           warning "Message does not have identification"
201         end
202       end
203       @logmessage = @message.dup
204       @plainmessage = BasicUserMessage.strip_formatting(@message)
205       @message = BasicUserMessage.strip_initial_formatting(@message)
206
207       if target && target == @bot.myself
208         @address = true
209       end
210
211     end
212
213     # Access the nick of the source
214     #
215     def sourcenick
216       @source.nick rescue @source.to_s
217     end
218
219     # Access the user@host of the source
220     #
221     def sourceaddress
222       "#{@source.user}@#{@source.host}" rescue @source.to_s
223     end
224
225     # Access the botuser corresponding to the source, if any
226     #
227     def botuser
228       source.botuser rescue @bot.auth.everyone
229     end
230
231
232     # Was the message from an identified user?
233     def identified?
234       return @identified
235     end
236
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.
241     def address?
242       return @address
243     end
244
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"
247     def prefixed?
248       return @prefixed
249     end
250
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", "")
256       ret
257     end
258
259     def BasicUserMessage.strip_initial_formatting(string)
260       return "" unless string
261       ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
262     end
263
264     def BasicUserMessage.strip_formatting(string)
265       string.gsub(FormattingRx,"")
266     end
267
268   end
269
270   # class for handling welcome messages from the server
271   class WelcomeMessage < BasicUserMessage
272   end
273
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
277   end
278
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
284
285     def inspect
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 << ')'
290       if self.private?
291         fields << ' (private)'
292       else
293         fields << ' (public)'
294       end
295       if self.action?
296         fields << ' (action)'
297       elsif ctcp
298         fields << ' (CTCP ' << ctcp << ')'
299       end
300       super(fields)
301     end
302
303     # for plugin messages, the name of the plugin invoked by the message
304     attr_reader :plugin
305
306     # for plugin messages, the rest of the message, with the plugin name
307     # removed
308     attr_reader :params
309
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
312     # addressed message
313     attr_reader :replyto
314
315     # channel the message was in, nil for privately addressed messages
316     attr_reader :channel
317
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.
322     attr_reader :ctcp
323
324     # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
325     # will be stripped from the message)
326     attr_reader :action
327
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)
335       @target = target
336       @private = false
337       @plugin = nil
338       @ctcp = false
339       @action = false
340
341       if target == @bot.myself
342         @private = true
343         @address = true
344         @channel = nil
345         @replyto = source
346       else
347         @replyto = @target
348         @channel = @target
349       end
350
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*/, "")
355           @address = true
356           @prefixed = true
357           break
358         end
359       }
360
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, "")
365         @address = true
366       end
367
368       if(@message =~ /^\001(\S+)(\s(.+))?\001/)
369         @ctcp = $1
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)
378       end
379
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
386       end
387     end
388
389     # returns true for private messages, e.g. "/msg bot hello"
390     def private?
391       return @private
392     end
393
394     # returns true if the message was in a channel
395     def public?
396       return !@private
397     end
398
399     def action?
400       return @action
401     end
402
403     # convenience method to reply to a message, useful in plugins. It's the
404     # same as doing:
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)
410     end
411
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)
416     end
417
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)
421     end
422
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
427     #
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.
431     #
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
439
440       if opts[:nick] == :auto
441         opts[:nick] = @bot.config['core.reply_with_nick']
442       end
443
444       if !self.public?
445         opts[:to] = :private
446       elsif opts[:to] == :auto
447         opts[:to] = @bot.config['core.private_replies'] ? :private : :public
448       end
449
450       if (opts[:nick] &&
451           opts[:to] != :private &&
452           (string !~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/ ||
453             opts[:forcenick]))
454         string = "#{@source}#{@bot.config['core.nick_postfix']} #{string}"
455       end
456       to = (opts[:to] == :private) ? source : @channel
457       @bot.say to, string, options
458       @replied = true
459     end
460
461     # convenience method to reply to a message with an action. It's the
462     # same as doing:
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
468       @replied = true
469     end
470
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
475     end
476
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
480     end
481
482     # Like the above, but append the username
483     def nick_literal(ident)
484       str = @bot.lang.get(ident).dup
485       if self.public?
486         # remove final punctuation
487         str.gsub!(/[!,.]$/,"")
488         str += ", #{@source}"
489       end
490       self.reply str, :nick => false
491     end
492
493     # the default okay style is the same as the default reply style
494     def okay
495       @bot.config['core.reply_with_nick'] ? nick_literal('okay') : plain_literal('okay')
496     end
497
498     # thanks the user in reply
499     def thanks
500       @bot.config['core.reply_with_nick'] ? nick_literal('thanks') : plain_literal('thanks')
501     end
502
503     # send a NOTICE to the message source
504     #
505     def notify(msg,opts={})
506       @bot.notice(sourcenick, msg, opts)
507     end
508
509   end
510
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)
516     end
517   end
518
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)
524     end
525   end
526
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
532     attr_reader :channel
533
534     def inspect
535       fields = ' channel=' << channel.to_s
536       super(fields)
537     end
538
539     def initialize(bot, server, source, target, channel, message="")
540       super(bot, server, source, target, message)
541       @channel = channel
542     end
543   end
544
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
550     attr_reader :channel
551
552     def inspect
553       fields = ' channel=' << channel.to_s
554       super(fields)
555     end
556
557     def initialize(bot, server, source, target, channel, message="")
558       super(bot, server, source, target, message)
559       @channel = channel
560     end
561   end
562
563   # class to pass IRC Nick changes in. @message contains the old nickame,
564   # @sourcenick contains the new one.
565   class NickMessage < BasicUserMessage
566     attr_accessor :is_on
567     def initialize(bot, server, source, oldnick, newnick)
568       super(bot, server, source, oldnick, newnick)
569       @address = (source == @bot.myself)
570       @is_on = []
571     end
572
573     def oldnick
574       return @target
575     end
576
577     def newnick
578       return @message
579     end
580
581     def inspect
582       fields = ' old=' << oldnick
583       fields << ' new=' << newnick
584       super(fields)
585     end
586   end
587
588   # class to manage mode changes
589   class ModeChangeMessage < BasicUserMessage
590     attr_accessor :modes
591     def initialize(bot, server, source, target, message="")
592       super(bot, server, source, target, message)
593       @address = (source == @bot.myself)
594       @modes = []
595     end
596
597     def inspect
598       fields = ' modes=' << modes.inspect
599       super(fields)
600     end
601   end
602
603   # class to manage WHOIS replies
604   class WhoisMessage < BasicUserMessage
605     attr_reader :whois
606     def initialize(bot, server, source, target, whois)
607       super(bot, server, source, target, "")
608       @address = (target == @bot.myself)
609       @whois = whois
610     end
611
612     def inspect
613       fields = ' whois=' << whois.inspect
614       super(fields)
615     end
616   end
617
618   # class to manage LIST replies
619   class ListMessage < BasicUserMessage
620     attr_accessor :list
621     def initialize(bot, server, source, target, list=Hash.new)
622       super(bot, server, source, target, "")
623       @list = []
624     end
625
626     def inspect
627       fields = ' list=' << list.inspect
628       super(fields)
629     end
630   end
631
632
633   # class to manage NAME replies
634   class NamesMessage < BasicUserMessage
635     attr_accessor :users
636     def initialize(bot, server, source, target, message="")
637       super(bot, server, source, target, message)
638       @users = []
639     end
640
641     def inspect
642       fields = ' users=' << users.inspect
643       super(fields)
644     end
645   end
646
647   # class to manager Ban list replies
648   class BanlistMessage < BasicUserMessage
649     # the bans
650     attr_accessor :bans
651
652     def initialize(bot, server, source, target, message="")
653       super(bot, server, source, target, message)
654       @bans = []
655     end
656
657     def inspect
658       fields = ' bans=' << bans.inspect
659       super(fields)
660     end
661   end
662
663   class QuitMessage < BasicUserMessage
664     attr_accessor :was_on
665     def initialize(bot, server, source, target, message="")
666       super(bot, server, source, target, message)
667       @was_on = []
668     end
669   end
670
671   class TopicMessage < BasicUserMessage
672     # channel topic
673     attr_reader :topic
674     # topic set at (unixtime)
675     attr_reader :timestamp
676     # topic set on channel
677     attr_reader :channel
678
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)
683       @topic = topic
684       @timestamp = topic.set_on
685       @channel = channel
686       @info_or_set = nil
687     end
688
689     def inspect
690       fields = ' topic=' << topic
691       fields << ' (set on ' << timestamp << ')'
692       super(fields)
693     end
694   end
695
696   # class to manage channel joins
697   class JoinMessage < BasicUserMessage
698     # channel joined
699     attr_reader :channel
700
701     def inspect
702       fields = ' channel=' << channel.to_s
703       super(fields)
704     end
705
706     def initialize(bot, server, source, channel, message="")
707       super(bot, server, source, channel, message)
708       @channel = channel
709       # in this case sourcenick is the nick that could be the bot
710       @address = (source == @bot.myself)
711     end
712   end
713
714   # class to manage channel parts
715   # same as a join, but can have a message too
716   class PartMessage < JoinMessage
717   end
718
719   # class to handle ERR_NOSUCHNICK and ERR_NOSUCHCHANNEL
720   class NoSuchTargetMessage < BasicUserMessage
721     # the channel or nick that was not found
722     attr_reader :target
723
724     def initialize(bot, server, source, target, message='')
725       super(bot, server, source, target, message)
726
727       @target = target
728     end
729   end
730
731   class UnknownMessage < BasicUserMessage
732   end
733 end