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