Minor messagemapper optimizations
[rbot] / lib / rbot / message.rb
1 module Irc
2   BotConfig.register BotConfigArrayValue.new('core.address_prefix',
3     :default => [], :wizard => true,
4     :desc => "what non nick-matching prefixes should the bot respond to as if addressed (e.g !, so that '!foo' is treated like 'rbot: foo')"
5   )
6
7   BotConfig.register BotConfigBooleanValue.new('core.reply_with_nick',
8     :default => false, :wizard => true,
9     :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!')"
10   )
11
12   BotConfig.register BotConfigStringValue.new('core.nick_postfix',
13     :default => ':', :wizard => true,
14     :desc => "when replying with nick put this character after the nick of the user the bot is replying to"
15   )
16
17   Color = "\003"
18   Bold = "\002"
19   Underline = "\037"
20   Reverse = "\026"
21
22   # base user message class, all user messages derive from this
23   # (a user message is defined as having a source hostmask, a target
24   # nick/channel and a message part)
25   class BasicUserMessage
26
27     # associated bot
28     attr_reader :bot
29
30     # associated server
31     attr_reader :server
32
33     # when the message was received
34     attr_reader :time
35
36     # User that originated the message
37     attr_reader :source
38
39     # User/Channel message was sent to
40     attr_reader :target
41
42     # contents of the message
43     attr_accessor :message
44
45     # has the message been replied to/handled by a plugin?
46     attr_accessor :replied
47
48     # instantiate a new Message
49     # bot::      associated bot class
50     # server::   Server where the message took place
51     # source::   User that sent the message
52     # target::   User/Channel is destined for
53     # message::  actual message
54     def initialize(bot, server, source, target, message)
55       @msg_wants_id = false unless defined? @msg_wants_id
56
57       @time = Time.now
58       @bot = bot
59       @source = source
60       @address = false
61       @target = target
62       @message = BasicUserMessage.stripcolour message
63       @replied = false
64       @server = server
65
66       @identified = false
67       if @msg_wants_id && @server.capabilities[:"identify-msg"]
68         if @message =~ /([-+])(.*)/
69           @identified = ($1=="+")
70           @message = $2
71         else
72           warning "Message does not have identification"
73         end
74       end
75
76       if target && target == @bot.myself
77         @address = true
78       end
79
80     end
81
82     # Access the nick of the source
83     #
84     def sourcenick
85       @source.nick
86     end
87
88     # Access the user@host of the source
89     #
90     def sourceaddress
91       "#{@source.user}@#{@source.host}"
92     end
93
94     # Was the message from an identified user?
95     def identified?
96       return @identified
97     end
98
99     # returns true if the message was addressed to the bot.
100     # This includes any private message to the bot, or any public message
101     # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
102     # a kick message when bot was kicked etc.
103     def address?
104       return @address
105     end
106
107     # has this message been replied to by a plugin?
108     def replied?
109       return @replied
110     end
111
112     # strip mIRC colour escapes from a string
113     def BasicUserMessage.stripcolour(string)
114       return "" unless string
115       ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "")
116       #ret.tr!("\x00-\x1f", "")
117       ret
118     end
119
120   end
121
122   # class for handling IRC user messages. Includes some utilities for handling
123   # the message, for example in plugins.
124   # The +message+ member will have any bot addressing "^bot: " removed
125   # (address? will return true in this case)
126   class UserMessage < BasicUserMessage
127
128     # for plugin messages, the name of the plugin invoked by the message
129     attr_reader :plugin
130
131     # for plugin messages, the rest of the message, with the plugin name
132     # removed
133     attr_reader :params
134
135     # convenience member. Who to reply to (i.e. would be sourcenick for a
136     # privately addressed message, or target (the channel) for a publicly
137     # addressed message
138     attr_reader :replyto
139
140     # channel the message was in, nil for privately addressed messages
141     attr_reader :channel
142
143     # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
144     # will be stripped from the message)
145     attr_reader :action
146
147     # instantiate a new UserMessage
148     # bot::      associated bot class
149     # source::   hostmask of the message source
150     # target::   nick/channel message is destined for
151     # message::  message part
152     def initialize(bot, server, source, target, message)
153       super(bot, server, source, target, message)
154       @target = target
155       @private = false
156       @plugin = nil
157       @action = false
158
159       if target == @bot.myself
160         @private = true
161         @address = true
162         @channel = nil
163         @replyto = source
164       else
165         @replyto = @target
166         @channel = @target
167       end
168
169       # check for option extra addressing prefixes, e.g "|search foo", or
170       # "!version" - first match wins
171       bot.config['core.address_prefix'].each {|mprefix|
172         if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
173           @address = true
174           break
175         end
176       }
177
178       # even if they used above prefixes, we allow for silly people who
179       # combine all possible types, e.g. "|rbot: hello", or
180       # "/msg rbot rbot: hello", etc
181       if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
182         @address = true
183       end
184
185       if(@message =~ /^\001ACTION\s(.+)\001/)
186         @message = $1
187         @action = true
188       end
189
190       # free splitting for plugins
191       @params = @message.dup
192       if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
193         @plugin = $1.downcase
194         @params = nil unless @params.length > 0
195       end
196     end
197
198     # returns true for private messages, e.g. "/msg bot hello"
199     def private?
200       return @private
201     end
202
203     # returns true if the message was in a channel
204     def public?
205       return !@private
206     end
207
208     def action?
209       return @action
210     end
211
212     # convenience method to reply to a message, useful in plugins. It's the
213     # same as doing:
214     # <tt>@bot.say m.replyto, string</tt>
215     # So if the message is private, it will reply to the user. If it was
216     # in a channel, it will reply in the channel.
217     def plainreply(string, options={})
218       @bot.say @replyto, string, options
219       @replied = true
220     end
221
222     # Same as reply, but when replying in public it adds the nick of the user
223     # the bot is replying to
224     def nickreply(string, options={})
225       extra = self.public? ? "#{@source}#{@bot.config['core.nick_postfix']} " : ""
226       @bot.say @replyto, extra + string, options
227       @replied = true
228     end
229
230     # the default reply style is to nickreply unless the reply already contains
231     # the nick or core.reply_with_nick is set to false
232     #
233     def reply(string, options={})
234       if @bot.config['core.reply_with_nick'] and not string =~ /\b#{@source}\b/
235         return nickreply(string, options)
236       end
237       plainreply(string, options)
238     end
239
240     # convenience method to reply to a message with an action. It's the
241     # same as doing:
242     # <tt>@bot.action m.replyto, string</tt>
243     # So if the message is private, it will reply to the user. If it was
244     # in a channel, it will reply in the channel.
245     def act(string, options={})
246       @bot.action @replyto, string, options
247       @replied = true
248     end
249
250     # convenience method to reply "okay" in the current language to the
251     # message
252     def plainokay
253       self.plainreply @bot.lang.get("okay")
254     end
255
256     # Like the above, but append the username
257     def nickokay
258       str = @bot.lang.get("okay").dup
259       if self.public?
260         # remove final punctuation
261         str.gsub!(/[!,.]$/,"")
262         str += ", #{@source}"
263       end
264       self.plainreply str
265     end
266
267     # the default okay style is the same as the default reply style
268     #
269     def okay
270       if @bot.config['core.reply_with_nick']
271         return nickokay
272       end
273       plainokay
274     end
275
276   end
277
278   # class to manage IRC PRIVMSGs
279   class PrivMessage < UserMessage
280     def initialize(bot, server, source, target, message)
281       @msg_wants_id = true
282       super
283     end
284   end
285
286   # class to manage IRC NOTICEs
287   class NoticeMessage < UserMessage
288     def initialize(bot, server, source, target, message)
289       @msg_wants_id = true
290       super
291     end
292   end
293
294   # class to manage IRC KICKs
295   # +address?+ can be used as a shortcut to see if the bot was kicked,
296   # basically, +target+ was kicked from +channel+ by +source+ with +message+
297   class KickMessage < BasicUserMessage
298     # channel user was kicked from
299     attr_reader :channel
300
301     def initialize(bot, server, source, target, channel, message="")
302       super(bot, server, source, target, message)
303       @channel = channel
304     end
305   end
306
307   # class to pass IRC Nick changes in. @message contains the old nickame,
308   # @sourcenick contains the new one.
309   class NickMessage < BasicUserMessage
310     def initialize(bot, server, source, oldnick, newnick)
311       super(bot, server, source, oldnick, newnick)
312     end
313
314     def oldnick
315       return @target
316     end
317
318     def newnick
319       return @message
320     end
321   end
322
323   class QuitMessage < BasicUserMessage
324     def initialize(bot, server, source, target, message="")
325       super(bot, server, source, target, message)
326     end
327   end
328
329   class TopicMessage < BasicUserMessage
330     # channel topic
331     attr_reader :topic
332     # topic set at (unixtime)
333     attr_reader :timestamp
334     # topic set on channel
335     attr_reader :channel
336
337     def initialize(bot, server, source, channel, topic=ChannelTopic.new)
338       super(bot, server, source, channel, topic.text)
339       @topic = topic
340       @timestamp = topic.set_on
341       @channel = channel
342     end
343   end
344
345   # class to manage channel joins
346   class JoinMessage < BasicUserMessage
347     # channel joined
348     attr_reader :channel
349     def initialize(bot, server, source, channel, message="")
350       super(bot, server, source, channel, message)
351       @channel = channel
352       # in this case sourcenick is the nick that could be the bot
353       @address = (source == @bot.myself)
354     end
355   end
356
357   # class to manage channel parts
358   # same as a join, but can have a message too
359   class PartMessage < JoinMessage
360   end
361 end