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