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