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