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