]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/message.rb
8b650681b96474b9e875ef8796fd0d291ce0b89f
[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     end
26   end
27
28
29   # Define standard IRC attriubtes (not so standard actually,
30   # but the closest thing we have ...)
31   Bold = "\002"
32   Underline = "\037"
33   Reverse = "\026"
34   Italic = "\011"
35   NormalText = "\017"
36   AttributeRx = /#{Bold}|#{Underline}|#{Reverse}|#{Italic}|#{NormalText}/
37
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
41   # must be present.
42   Color = "\003"
43   ColorRx = /#{Color}\d?\d?(?:,\d\d?)?/
44
45   FormattingRx = /#{AttributeRx}|#{ColorRx}/
46
47   # Standard color codes
48   ColorCode = {
49     :black      => 1,
50     :blue       => 2,
51     :navyblue   => 2,
52     :navy_blue  => 2,
53     :green      => 3,
54     :red        => 4,
55     :brown      => 5,
56     :purple     => 6,
57     :olive      => 7,
58     :yellow     => 8,
59     :limegreen  => 9,
60     :lime_green => 9,
61     :teal       => 10,
62     :aqualight  => 11,
63     :aqua_light => 11,
64     :royal_blue => 12,
65     :hotpink    => 13,
66     :hot_pink   => 13,
67     :darkgray   => 14,
68     :dark_gray  => 14,
69     :lightgray  => 15,
70     :light_gray => 15,
71     :white      => 16
72   }
73
74   # Convert a String or Symbol into a color number
75   def Irc.find_color(data)
76     "%02d" % if Integer === data
77       data
78     else
79       f = if String === data
80             data.intern
81           else
82             data
83           end
84       if ColorCode.key?(f)
85         ColorCode[f] 
86       else
87         0
88       end
89     end
90   end
91
92   # Insert the full color code for a given
93   # foreground/background combination.
94   def Irc.color(fg=nil,bg=nil)
95     str = Color.dup
96     if fg
97      str << Irc.find_color(fg)
98     end
99     if bg
100       str << "," << Irc.find_color(bg)
101     end
102     return str
103   end
104
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
109
110     # associated bot
111     attr_reader :bot
112
113     # associated server
114     attr_reader :server
115
116     # when the message was received
117     attr_reader :time
118
119     # User that originated the message
120     attr_reader :source
121
122     # User/Channel message was sent to
123     attr_reader :target
124
125     # contents of the message (stripped of initial/final format codes)
126     attr_accessor :message
127
128     # contents of the message (for logging purposes)
129     attr_accessor :logmessage
130
131     # contents of the message (stripped of all formatting)
132     attr_accessor :plainmessage
133
134     # has the message been replied to/handled by a plugin?
135     attr_accessor :replied
136     alias :replied? :replied
137
138     # should the message be ignored?
139     attr_accessor :ignored
140     alias :ignored? :ignored
141
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
145
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?
162       ret << '>'
163     end
164
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
173
174       @time = Time.now
175       @bot = bot
176       @source = source
177       @address = false
178       @target = target
179       @message = message || ""
180       @replied = false
181       @server = server
182       @ignored = false
183       @in_thread = false
184
185       @identified = false
186       if @msg_wants_id && @server.capabilities[:"identify-msg"]
187         if @message =~ /^([-+])(.*)/
188           @identified = ($1=="+")
189           @message = $2
190         else
191           warning "Message does not have identification"
192         end
193       end
194       @logmessage = @message.dup
195       @plainmessage = BasicUserMessage.strip_formatting(@message)
196       @message = BasicUserMessage.strip_initial_formatting(@message)
197
198       if target && target == @bot.myself
199         @address = true
200       end
201
202     end
203
204     # Access the nick of the source
205     #
206     def sourcenick
207       @source.nick rescue @source.to_s
208     end
209
210     # Access the user@host of the source
211     #
212     def sourceaddress
213       "#{@source.user}@#{@source.host}" rescue @source.to_s
214     end
215
216     # Access the botuser corresponding to the source, if any
217     #
218     def botuser
219       source.botuser rescue @bot.auth.everyone
220     end
221
222
223     # Was the message from an identified user?
224     def identified?
225       return @identified
226     end
227
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.
232     def address?
233       return @address
234     end
235
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", "")
241       ret
242     end
243
244     def BasicUserMessage.strip_initial_formatting(string)
245       return "" unless string
246       ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
247     end
248
249     def BasicUserMessage.strip_formatting(string)
250       string.gsub(FormattingRx,"")
251     end
252
253   end
254
255   # class for handling welcome messages from the server
256   class WelcomeMessage < BasicUserMessage
257   end
258
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
262   end
263
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
269
270     def inspect
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 << ')'
275       if self.private?
276         fields << ' (private)'
277       else
278         fields << ' (public)'
279       end
280       if self.action?
281         fields << ' (action)'
282       elsif ctcp
283         fields << ' (CTCP ' << ctcp << ')'
284       end
285       super(fields)
286     end
287
288     # for plugin messages, the name of the plugin invoked by the message
289     attr_reader :plugin
290
291     # for plugin messages, the rest of the message, with the plugin name
292     # removed
293     attr_reader :params
294
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
297     # addressed message
298     attr_reader :replyto
299
300     # channel the message was in, nil for privately addressed messages
301     attr_reader :channel
302
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.
307     attr_reader :ctcp
308
309     # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
310     # will be stripped from the message)
311     attr_reader :action
312
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)
320       @target = target
321       @private = false
322       @plugin = nil
323       @ctcp = false
324       @action = false
325
326       if target == @bot.myself
327         @private = true
328         @address = true
329         @channel = nil
330         @replyto = source
331       else
332         @replyto = @target
333         @channel = @target
334       end
335
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*/, "")
340           @address = true
341           break
342         end
343       }
344
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, "")
349         @address = true
350       end
351
352       if(@message =~ /^\001(\S+)(\s(.+))?\001/)
353         @ctcp = $1
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)
362       end
363
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
370       end
371     end
372
373     # returns true for private messages, e.g. "/msg bot hello"
374     def private?
375       return @private
376     end
377
378     # returns true if the message was in a channel
379     def public?
380       return !@private
381     end
382
383     def action?
384       return @action
385     end
386
387     # convenience method to reply to a message, useful in plugins. It's the
388     # same as doing:
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)
394     end
395
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)
400     end
401
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
406     #
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']
411       end
412       if (opts[:nick] && self.public? &&
413           string !~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/)
414         string = "#{@source}#{@bot.config['core.nick_postfix']} #{string}"
415       end
416       @bot.say @replyto, string, options
417       @replied = true
418     end
419
420     # convenience method to reply to a message with an action. It's the
421     # same as doing:
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
427       @replied = true
428     end
429
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
434     end
435
436     # convenience method to reply "okay" in the current language to the
437     # message
438     def plainokay
439       self.reply @bot.lang.get("okay"), :nick => false
440     end
441
442     # Like the above, but append the username
443     def nickokay
444       str = @bot.lang.get("okay").dup
445       if self.public?
446         # remove final punctuation
447         str.gsub!(/[!,.]$/,"")
448         str += ", #{@source}"
449       end
450       self.reply str, :nick => false
451     end
452
453     # the default okay style is the same as the default reply style
454     #
455     def okay
456       @bot.config['core.reply_with_nick'] ? nickokay : plainokay
457     end
458
459     # send a NOTICE to the message source
460     #
461     def notify(msg,opts={})
462       @bot.notice(sourcenick, msg, opts)
463     end
464
465   end
466
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)
472     end
473   end
474
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)
480     end
481   end
482
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
488     attr_reader :channel
489
490     def inspect
491       fields = ' channel=' << channel.to_s
492       super(fields)
493     end
494
495     def initialize(bot, server, source, target, channel, message="")
496       super(bot, server, source, target, message)
497       @channel = channel
498     end
499   end
500
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
506     attr_reader :channel
507
508     def inspect
509       fields = ' channel=' << channel.to_s
510       super(fields)
511     end
512
513     def initialize(bot, server, source, target, channel, message="")
514       super(bot, server, source, target, message)
515       @channel = channel
516     end
517   end
518
519   # class to pass IRC Nick changes in. @message contains the old nickame,
520   # @sourcenick contains the new one.
521   class NickMessage < BasicUserMessage
522     attr_accessor :is_on
523     def initialize(bot, server, source, oldnick, newnick)
524       super(bot, server, source, oldnick, newnick)
525       @address = (source == @bot.myself)
526       @is_on = []
527     end
528
529     def oldnick
530       return @target
531     end
532
533     def newnick
534       return @message
535     end
536
537     def inspect
538       fields = ' old=' << oldnick
539       fields << ' new=' << newnick
540       super(fields)
541     end
542   end
543
544   # class to manage mode changes
545   class ModeChangeMessage < BasicUserMessage
546     attr_accessor :modes
547     def initialize(bot, server, source, target, message="")
548       super(bot, server, source, target, message)
549       @address = (source == @bot.myself)
550       @modes = []
551     end
552
553     def inspect
554       fields = ' modes=' << modes.inspect
555       super(fields)
556     end
557   end
558
559   # class to manage WHOIS replies
560   class WhoisMessage < BasicUserMessage
561     attr_reader :whois
562     def initialize(bot, server, source, target, whois)
563       super(bot, server, source, target, "")
564       @address = (target == @bot.myself)
565       @whois = whois
566     end
567
568     def inspect
569       fields = ' whois=' << whois.inspect
570       super(fields)
571     end
572   end
573
574   # class to manage NAME replies
575   class NamesMessage < BasicUserMessage
576     attr_accessor :users
577     def initialize(bot, server, source, target, message="")
578       super(bot, server, source, target, message)
579       @users = []
580     end
581
582     def inspect
583       fields = ' users=' << users.inspect
584       super(fields)
585     end
586   end
587
588   class QuitMessage < BasicUserMessage
589     attr_accessor :was_on
590     def initialize(bot, server, source, target, message="")
591       super(bot, server, source, target, message)
592       @was_on = []
593     end
594   end
595
596   class TopicMessage < BasicUserMessage
597     # channel topic
598     attr_reader :topic
599     # topic set at (unixtime)
600     attr_reader :timestamp
601     # topic set on channel
602     attr_reader :channel
603
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)
608       @topic = topic
609       @timestamp = topic.set_on
610       @channel = channel
611       @info_or_set = nil
612     end
613
614     def inspect
615       fields = ' topic=' << topic
616       fields << ' (set on ' << timestamp << ')'
617       super(fields)
618     end
619   end
620
621   # class to manage channel joins
622   class JoinMessage < BasicUserMessage
623     # channel joined
624     attr_reader :channel
625
626     def inspect
627       fields = ' channel=' << channel.to_s
628       super(fields)
629     end
630
631     def initialize(bot, server, source, channel, message="")
632       super(bot, server, source, channel, message)
633       @channel = channel
634       # in this case sourcenick is the nick that could be the bot
635       @address = (source == @bot.myself)
636     end
637   end
638
639   # class to manage channel parts
640   # same as a join, but can have a message too
641   class PartMessage < JoinMessage
642   end
643
644   class UnknownMessage < BasicUserMessage
645   end
646 end