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