]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/message.rb
47994c10735d4e5a01ab8ae2fdc4aad1b49ae6b8
[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       end
291
292       # free splitting for plugins
293       @params = @message.dup
294       if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
295         @plugin = $1.downcase
296         @params = nil unless @params.length > 0
297       end
298     end
299
300     # returns true for private messages, e.g. "/msg bot hello"
301     def private?
302       return @private
303     end
304
305     # returns true if the message was in a channel
306     def public?
307       return !@private
308     end
309
310     def action?
311       return @action
312     end
313
314     # convenience method to reply to a message, useful in plugins. It's the
315     # same as doing:
316     # <tt>@bot.say m.replyto, string</tt>
317     # So if the message is private, it will reply to the user. If it was
318     # in a channel, it will reply in the channel.
319     def plainreply(string, options={})
320       @bot.say @replyto, string, options
321       @replied = true
322     end
323
324     # Same as reply, but when replying in public it adds the nick of the user
325     # the bot is replying to
326     def nickreply(string, options={})
327       extra = self.public? ? "#{@source}#{@bot.config['core.nick_postfix']} " : ""
328       @bot.say @replyto, extra + string, options
329       @replied = true
330     end
331
332     # the default reply style is to nickreply unless the reply already contains
333     # the nick or core.reply_with_nick is set to false
334     #
335     def reply(string, options={})
336       if @bot.config['core.reply_with_nick'] and not string =~ /\b#{@source}\b/
337         return nickreply(string, options)
338       end
339       plainreply(string, options)
340     end
341
342     # convenience method to reply to a message with an action. It's the
343     # same as doing:
344     # <tt>@bot.action m.replyto, string</tt>
345     # So if the message is private, it will reply to the user. If it was
346     # in a channel, it will reply in the channel.
347     def act(string, options={})
348       @bot.action @replyto, string, options
349       @replied = true
350     end
351
352     # send a CTCP response, i.e. a private NOTICE to the sender
353     # with the same CTCP command and the reply as a parameter
354     def ctcp_reply(string, options={})
355       @bot.ctcp_notice @source, @ctcp, string, options
356     end
357
358     # convenience method to reply "okay" in the current language to the
359     # message
360     def plainokay
361       self.plainreply @bot.lang.get("okay")
362     end
363
364     # Like the above, but append the username
365     def nickokay
366       str = @bot.lang.get("okay").dup
367       if self.public?
368         # remove final punctuation
369         str.gsub!(/[!,.]$/,"")
370         str += ", #{@source}"
371       end
372       self.plainreply str
373     end
374
375     # the default okay style is the same as the default reply style
376     #
377     def okay
378       if @bot.config['core.reply_with_nick']
379         return nickokay
380       end
381       plainokay
382     end
383
384   end
385
386   # class to manage IRC PRIVMSGs
387   class PrivMessage < UserMessage
388     def initialize(bot, server, source, target, message)
389       @msg_wants_id = true
390       super
391     end
392   end
393
394   # class to manage IRC NOTICEs
395   class NoticeMessage < UserMessage
396     def initialize(bot, server, source, target, message)
397       @msg_wants_id = true
398       super
399     end
400   end
401
402   # class to manage IRC KICKs
403   # +address?+ can be used as a shortcut to see if the bot was kicked,
404   # basically, +target+ was kicked from +channel+ by +source+ with +message+
405   class KickMessage < BasicUserMessage
406     # channel user was kicked from
407     attr_reader :channel
408
409     def initialize(bot, server, source, target, channel, message="")
410       super(bot, server, source, target, message)
411       @channel = channel
412     end
413   end
414
415   # class to pass IRC Nick changes in. @message contains the old nickame,
416   # @sourcenick contains the new one.
417   class NickMessage < BasicUserMessage
418     def initialize(bot, server, source, oldnick, newnick)
419       super(bot, server, source, oldnick, newnick)
420     end
421
422     def oldnick
423       return @target
424     end
425
426     def newnick
427       return @message
428     end
429   end
430
431   class QuitMessage < BasicUserMessage
432     def initialize(bot, server, source, target, message="")
433       super(bot, server, source, target, message)
434     end
435   end
436
437   class TopicMessage < BasicUserMessage
438     # channel topic
439     attr_reader :topic
440     # topic set at (unixtime)
441     attr_reader :timestamp
442     # topic set on channel
443     attr_reader :channel
444
445     def initialize(bot, server, source, channel, topic=ChannelTopic.new)
446       super(bot, server, source, channel, topic.text)
447       @topic = topic
448       @timestamp = topic.set_on
449       @channel = channel
450     end
451   end
452
453   # class to manage channel joins
454   class JoinMessage < BasicUserMessage
455     # channel joined
456     attr_reader :channel
457     def initialize(bot, server, source, channel, message="")
458       super(bot, server, source, channel, message)
459       @channel = channel
460       # in this case sourcenick is the nick that could be the bot
461       @address = (source == @bot.myself)
462     end
463   end
464
465   # class to manage channel parts
466   # same as a join, but can have a message too
467   class PartMessage < JoinMessage
468   end
469 end