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