]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/message.rb
message handling: improve IRC format handling for received messages
[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
137     # should the message be ignored?
138     attr_accessor :ignored
139     alias :ignored? :ignored
140
141     # set this to true if the method that delegates the message is run in a thread
142     attr_accessor :in_thread
143     alias :in_thread? :in_thread
144
145     # instantiate a new Message
146     # bot::      associated bot class
147     # server::   Server where the message took place
148     # source::   User that sent the message
149     # target::   User/Channel is destined for
150     # message::  actual message
151     def initialize(bot, server, source, target, message)
152       @msg_wants_id = false unless defined? @msg_wants_id
153
154       @time = Time.now
155       @bot = bot
156       @source = source
157       @address = false
158       @target = target
159       @message = message || ""
160       @replied = false
161       @server = server
162       @ignored = false
163       @in_thread = false
164
165       @identified = false
166       if @msg_wants_id && @server.capabilities[:"identify-msg"]
167         if @message =~ /^([-+])(.*)/
168           @identified = ($1=="+")
169           @message = $2
170         else
171           warning "Message does not have identification"
172         end
173       end
174       @logmessage = @message.dup
175       @plainmessage = BasicUserMessage.strip_formatting(@message)
176       @message = BasicUserMessage.strip_initial_formatting(@message)
177
178       if target && target == @bot.myself
179         @address = true
180       end
181
182     end
183
184     # Access the nick of the source
185     #
186     def sourcenick
187       @source.nick rescue @source.to_s
188     end
189
190     # Access the user@host of the source
191     #
192     def sourceaddress
193       "#{@source.user}@#{@source.host}" rescue @source.to_s
194     end
195
196     # Access the botuser corresponding to the source, if any
197     #
198     def botuser
199       source.botuser rescue @bot.auth.everyone
200     end
201
202
203     # Was the message from an identified user?
204     def identified?
205       return @identified
206     end
207
208     # returns true if the message was addressed to the bot.
209     # This includes any private message to the bot, or any public message
210     # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
211     # a kick message when bot was kicked etc.
212     def address?
213       return @address
214     end
215
216     # has this message been replied to by a plugin?
217     def replied?
218       return @replied
219     end
220
221     # strip mIRC colour escapes from a string
222     def BasicUserMessage.stripcolour(string)
223       return "" unless string
224       ret = string.gsub(ColorRx, "")
225       #ret.tr!("\x00-\x1f", "")
226       ret
227     end
228
229     def BasicUserMessage.strip_initial_formatting(string)
230       return "" unless string
231       ret = string.gsub(/^#{FormattingRx}|#{FormattingRx}$/,"")
232     end
233
234     def BasicUserMessage.strip_formatting(string)
235       string.gsub(FormattingRx,"")
236     end
237
238   end
239
240   # class for handling welcome messages from the server
241   class WelcomeMessage < BasicUserMessage
242   end
243
244   # class for handling MOTD from the server. Yes, MotdMessage
245   # is somewhat redundant, but it fits with the naming scheme
246   class MotdMessage < BasicUserMessage
247   end
248
249   # class for handling IRC user messages. Includes some utilities for handling
250   # the message, for example in plugins.
251   # The +message+ member will have any bot addressing "^bot: " removed
252   # (address? will return true in this case)
253   class UserMessage < BasicUserMessage
254
255     # for plugin messages, the name of the plugin invoked by the message
256     attr_reader :plugin
257
258     # for plugin messages, the rest of the message, with the plugin name
259     # removed
260     attr_reader :params
261
262     # convenience member. Who to reply to (i.e. would be sourcenick for a
263     # privately addressed message, or target (the channel) for a publicly
264     # addressed message
265     attr_reader :replyto
266
267     # channel the message was in, nil for privately addressed messages
268     attr_reader :channel
269
270     # for PRIVMSGs, false unless the message was a CTCP command,
271     # in which case it evaluates to the CTCP command itself
272     # (TIME, PING, VERSION, etc). The CTCP command parameters
273     # are then stored in the message.
274     attr_reader :ctcp
275
276     # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
277     # will be stripped from the message)
278     attr_reader :action
279
280     # instantiate a new UserMessage
281     # bot::      associated bot class
282     # source::   hostmask of the message source
283     # target::   nick/channel message is destined for
284     # message::  message part
285     def initialize(bot, server, source, target, message)
286       super(bot, server, source, target, message)
287       @target = target
288       @private = false
289       @plugin = nil
290       @ctcp = false
291       @action = false
292
293       if target == @bot.myself
294         @private = true
295         @address = true
296         @channel = nil
297         @replyto = source
298       else
299         @replyto = @target
300         @channel = @target
301       end
302
303       # check for option extra addressing prefixes, e.g "|search foo", or
304       # "!version" - first match wins
305       bot.config['core.address_prefix'].each {|mprefix|
306         if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
307           @address = true
308           break
309         end
310       }
311
312       # even if they used above prefixes, we allow for silly people who
313       # combine all possible types, e.g. "|rbot: hello", or
314       # "/msg rbot rbot: hello", etc
315       if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
316         @address = true
317       end
318
319       if(@message =~ /^\001(\S+)(\s(.+))?\001/)
320         @ctcp = $1
321         # FIXME need to support quoting of NULL and CR/LF, see
322         # http://www.irchelp.org/irchelp/rfc/ctcpspec.html
323         @message = $3 || String.new
324         @action = @ctcp == 'ACTION'
325         debug "Received CTCP command #{@ctcp} with options #{@message} (action? #{@action})"
326         @logmessage = @message.dup
327       end
328
329       # free splitting for plugins
330       @params = @message.dup
331       if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
332         @plugin = $1.downcase
333         @params = nil unless @params.length > 0
334       end
335     end
336
337     # returns true for private messages, e.g. "/msg bot hello"
338     def private?
339       return @private
340     end
341
342     # returns true if the message was in a channel
343     def public?
344       return !@private
345     end
346
347     def action?
348       return @action
349     end
350
351     # convenience method to reply to a message, useful in plugins. It's the
352     # same as doing:
353     # <tt>@bot.say m.replyto, string</tt>
354     # So if the message is private, it will reply to the user. If it was
355     # in a channel, it will reply in the channel.
356     def plainreply(string, options={})
357       @bot.say @replyto, string, options
358       @replied = true
359     end
360
361     # Same as reply, but when replying in public it adds the nick of the user
362     # the bot is replying to
363     def nickreply(string, options={})
364       extra = self.public? ? "#{@source}#{@bot.config['core.nick_postfix']} " : ""
365       @bot.say @replyto, extra + string, options
366       @replied = true
367     end
368
369     # the default reply style is to nickreply unless the reply already contains
370     # the nick or core.reply_with_nick is set to false
371     #
372     def reply(string, options={})
373       if @bot.config['core.reply_with_nick'] and not string =~ /(?:^|\W)#{Regexp.escape(@source.to_s)}(?:$|\W)/
374         return nickreply(string, options)
375       end
376       plainreply(string, options)
377     end
378
379     # convenience method to reply to a message with an action. It's the
380     # same as doing:
381     # <tt>@bot.action m.replyto, string</tt>
382     # So if the message is private, it will reply to the user. If it was
383     # in a channel, it will reply in the channel.
384     def act(string, options={})
385       @bot.action @replyto, string, options
386       @replied = true
387     end
388
389     # send a CTCP response, i.e. a private NOTICE to the sender
390     # with the same CTCP command and the reply as a parameter
391     def ctcp_reply(string, options={})
392       @bot.ctcp_notice @source, @ctcp, string, options
393     end
394
395     # convenience method to reply "okay" in the current language to the
396     # message
397     def plainokay
398       self.plainreply @bot.lang.get("okay")
399     end
400
401     # Like the above, but append the username
402     def nickokay
403       str = @bot.lang.get("okay").dup
404       if self.public?
405         # remove final punctuation
406         str.gsub!(/[!,.]$/,"")
407         str += ", #{@source}"
408       end
409       self.plainreply str
410     end
411
412     # the default okay style is the same as the default reply style
413     #
414     def okay
415       if @bot.config['core.reply_with_nick']
416         return nickokay
417       end
418       plainokay
419     end
420
421     # send a NOTICE to the message source
422     #
423     def notify(msg,opts={})
424       @bot.notice(sourcenick, msg, opts)
425     end
426
427   end
428
429   # class to manage IRC PRIVMSGs
430   class PrivMessage < UserMessage
431     def initialize(bot, server, source, target, message)
432       @msg_wants_id = true
433       super
434     end
435   end
436
437   # class to manage IRC NOTICEs
438   class NoticeMessage < UserMessage
439     def initialize(bot, server, source, target, message)
440       @msg_wants_id = true
441       super
442     end
443   end
444
445   # class to manage IRC KICKs
446   # +address?+ can be used as a shortcut to see if the bot was kicked,
447   # basically, +target+ was kicked from +channel+ by +source+ with +message+
448   class KickMessage < BasicUserMessage
449     # channel user was kicked from
450     attr_reader :channel
451
452     def initialize(bot, server, source, target, channel, message="")
453       super(bot, server, source, target, message)
454       @channel = channel
455     end
456   end
457
458   # class to manage IRC INVITEs
459   # +address?+ can be used as a shortcut to see if the bot was invited,
460   # which should be true except for server bugs
461   class InviteMessage < BasicUserMessage
462     # channel user was invited to
463     attr_reader :channel
464
465     def initialize(bot, server, source, target, channel, message="")
466       super(bot, server, source, target, message)
467       @channel = channel
468     end
469   end
470
471   # class to pass IRC Nick changes in. @message contains the old nickame,
472   # @sourcenick contains the new one.
473   class NickMessage < BasicUserMessage
474     attr_accessor :is_on
475     def initialize(bot, server, source, oldnick, newnick)
476       super(bot, server, source, oldnick, newnick)
477       @is_on = []
478     end
479
480     def oldnick
481       return @target
482     end
483
484     def newnick
485       return @message
486     end
487   end
488
489   # class to manage mode changes
490   class ModeChangeMessage < BasicUserMessage
491     attr_accessor :modes
492     def initialize(bot, server, source, target, message="")
493       super(bot, server, source, target, message)
494       @address = (source == @bot.myself)
495       @modes = []
496     end
497   end
498
499   # class to manage NAME replies
500   class NamesMessage < BasicUserMessage
501     attr_accessor :users
502     def initialize(bot, server, source, target, message="")
503       super(bot, server, source, target, message)
504       @users = []
505     end
506   end
507
508   class QuitMessage < BasicUserMessage
509     attr_accessor :was_on
510     def initialize(bot, server, source, target, message="")
511       super(bot, server, source, target, message)
512       @was_on = []
513     end
514   end
515
516   class TopicMessage < BasicUserMessage
517     # channel topic
518     attr_reader :topic
519     # topic set at (unixtime)
520     attr_reader :timestamp
521     # topic set on channel
522     attr_reader :channel
523
524     # :info if topic info, :set if topic set
525     attr_accessor :info_or_set
526     def initialize(bot, server, source, channel, topic=ChannelTopic.new)
527       super(bot, server, source, channel, topic.text)
528       @topic = topic
529       @timestamp = topic.set_on
530       @channel = channel
531       @info_or_set = nil
532     end
533   end
534
535   # class to manage channel joins
536   class JoinMessage < BasicUserMessage
537     # channel joined
538     attr_reader :channel
539     def initialize(bot, server, source, channel, message="")
540       super(bot, server, source, channel, message)
541       @channel = channel
542       # in this case sourcenick is the nick that could be the bot
543       @address = (source == @bot.myself)
544     end
545   end
546
547   # class to manage channel parts
548   # same as a join, but can have a message too
549   class PartMessage < JoinMessage
550   end
551
552   class UnknownMessage < BasicUserMessage
553   end
554 end