3 # CyBot plugin root class.
9 CmdFlag_Normal = 0 # Normal command.
10 CmdFlag_Channel = 1 # Channel-bound command.
11 CmdFlag_Server = 2 # Server-bound command.
12 CmdFlag_Multiple = 4 # Command exists in multiple plugins (exclusive flag).
14 # Override this to do weird stuff :-p.
15 def PluginBase.shared_instance
16 @instance ||= self.new
19 # Force a new instance to be created. Used when loading.
20 def PluginBase.instance
24 # Reload support. Called on the _instance_.
25 # Before: Called just before reload would take place. Returning a string will disallow
26 # the reload with that as a reason.
27 # After: Called right after the reload. Returning true will create a new instance for
28 # the plugin. Remeber to save in that case, since the new instance will invoke
31 def before_reload(forced = false)
34 def after_reload(forced = false)
38 # Called when a reload fails due to compile errors. Return a string to use as error,
39 # or nil for the default.
40 def reload_failed(exception)
44 attr_reader :commands, :hooks, :brief_help
45 attr_accessor :sensitive_command
48 def register_command(name, method_name, flags, help_map)
49 cmd = [flags, method(method_name.to_sym), self, help_map && help_map[name.to_sym]]
51 if (c = $commands[name])
52 if c[0] != CmdFlag_Multiple
53 $commands[name] = (c = [CmdFlag_Multiple, c])
56 elsif (p = $plugins[name])
57 $commands[name] = [CmdFlag_Multiple, cmd]
63 # Register commands for the given plugin instance.
66 # Step 1: Register new commands.
67 help_map = self.class.help_map
68 self.class.instance_methods(false).each do |name|
70 if name =~ /^cmd_(.*)$/
71 register_command($1, name, CmdFlag_Normal, help_map)
72 elsif name =~ /^serv_(.*)$/
73 register_command($1, name, CmdFlag_Server, help_map)
74 elsif name =~ /^chan_(.*)$/
75 register_command($1, name, CmdFlag_Channel, help_map)
76 elsif name =~ /^hook_(.*)$/
78 h = [method(name.to_sym), hook, self]
79 hook_list = $hooks[hook] || ($hooks[hook] = [])
85 # Step 2: Fix-up old commands to avoid ambiguity.
86 if @commands.length > 0 and (c = $commands[name]) and c[0] != CmdFlag_Multiple
87 $commands[name] = [CmdFlag_Multiple, c]
91 self.class.help_map = nil
95 # We need to be able to call this.
100 # Unregister commands and hooks for this plugin. Override to do dispose stuff.
105 @commands.each do |k,v|
109 when CmdFlag_Normal: 'cmd_'
110 when CmdFlag_Channel: 'chan_'
111 when CmdFlag_Server: 'serv_'
113 klass.remove_method name.to_sym
115 # Remove from global maps.
116 if (cmd = $commands[k])
117 if cmd[0] != CmdFlag_Multiple
118 $commands.delete(k) if cmd[2].class == klass
120 j = cmd.each_with_index do |c, i|
122 break i if c[2].class == klass
124 cmd.delete_at(j) if j != cmd
132 klass.remove_method "hook_#{h[1]}".to_sym
133 if (hook = $hooks[h[1]])
134 hook.delete_if { |e| e[2].class == klass }
144 # Only override this if you know what you're doing.
146 @name ||= self.class.name.downcase
149 # Remember to call this to get your commands regged.
153 @brief_help = "#{name.capitalize} plugin." unless @brief_help
158 # Return the relative filename for this plugin.
159 def file_name(name = nil)
160 dir = "data/#{self.class.name.downcase}"
161 dir << '/' if name and name[0] != ?/
165 # Open or create a plugin data-file. Can accept a block.
166 def open_file(name, mode = 'r', &block)
167 # Check that data exists
168 Dir.mkdir('data') unless File.directory?('data')
170 path = "data/#{self.class.name.downcase}"
171 Dir.mkdir(path) unless File.directory?(path)
172 File.open("#{path}/#{name}", mode, &block)
175 # Load/save plugin-state. Override to do anything here.
176 # These are called on init and before the bot quits.
182 # Command help generator.
185 hm = @help_map || (@help_map = {})
188 attr_accessor :help_map
191 # Command parser wrapper generator.
194 # channel A channel the bot is on, or automatically
195 # set to the channel the command is called from.
196 # anychannel Arbitrary channel name.
197 # integer An integer.
198 # text Any text, possibly empty. Must be last argument.
199 # text_ne Any (non-empty) text. Ditto.
201 def PluginBase.parse(name, *args)
203 # No args, nothing to do.
204 return unless args.length > 0
207 new_name = '__parse_' + name.to_s
208 alias_method(new_name.to_sym, name.to_sym)
210 # And put in our new one.
212 def #{name.to_s}(irc, line)
213 #{new_name}(irc, *parse(irc, line, #{args.map {|a| a.kind_of?(String) ?
214 ('"' + a + '"') : (':' + a.to_s)}.join(', ')}))
217 # Kernel.puts "Genrated code: " + code
222 # Argument parse error.
223 class ParseError < Exception
224 def initialize(msg, arg_num, usage, format)
225 super("Argument #{arg_num + 1}: #{msg}")
230 @usage ? @usage : (@format.map { |f|
232 when :nick: '<nick name>'
233 when :channel: '[bot channel]'
234 when :anychannel: '<channel>'
235 when :integer: '<integer>'
236 when :text: '[string]'
237 when :text_ne: '<string>'
244 def parse(irc, line, *forms)
245 usage = forms[0].kind_of?(String) ? forms.delete_at(0) : nil
248 forms.length.times do |i|
251 # Text. Last argument.
252 if f == :text or f == :text_ne
253 line = line.strip if line
254 if f == :text_ne and (!line or line.length == 0)
255 raise ParseError.new('Non-empty string expected.', i, usage, forms)
257 args << (line ? line : '')
260 # Current- or bot-channel.
265 if line: word, line = line.split(' ', 2)
267 unless word and (c = irc.server.channels[word])
268 raise ParseError.new('Bot-channel name expected.', i, usage, forms)
275 if line: word, line = line.split(' ', 2)
277 unless word and word.length > 0
278 raise ParseError.new('Not enough arguments.', i, usage, forms)
288 unless word and word.length > 0 and (word[0] == ?# or word[0] == ?&)
289 raise ParseError.new('Channel name expected.', i, usage, forms)
299 raise ParseError.new('Integer expected.', i, usage, forms)
310 WeekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
312 days, now = ts.to_i / (24*60*60), Time.now.to_i / (24*60*60)
313 if days == now: "at #{ts.strftime('%H:%M')}"
314 elsif days + 1 == now: "yesterday at #{ts.strftime('%H:%M')}"
315 elsif days + 6 <= now: "#{WeekDays[ts.wday]} at #{ts.strftime('%H:%M')}"
316 else "on #{ts.strftime('%m-%d %H:%M')}"
320 # Evaluate a setting (or settings) on channel and server levels, with optional
321 # global level provided by the passed block.
322 def setting_cs(irc, name = nil)
324 if (s = $config["servers/#{irc.server.name}"])
325 if (c = s['channels']) and (c = c[irc.channel.name])
326 a = name ? c[name] : yield(c)
329 a = name ? s[name] : yield(s)
332 if a.nil? and block_given?
333 a = name ? yield : yield(nil)
339 # ---------------------------------------------------------------------------
340 # Time duration formatter.
341 # ---------------------------------------------------------------------------
343 X_TimeWords = %w{ one two three four five six seven eight nine ten eleven twelve } # thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty
346 [ "year", 365 * 60 * 60 * 24 ],
347 [ "month", 30.4 * 60 * 60 * 24 ],
348 [ "week", 7 * 60 * 60 * 24 ],
349 [ "day", 24 * 60 * 60 ],
355 X_TimePrefixes = { :greater => "almost ", :less => "about " }
357 def _round_time(seconds, unit_size)
358 count = seconds.to_f / unit_size
360 when 0.0: [ count.to_i, :exact ]
361 when 0.0..0.3: [ count.floor, :less ]
362 when 0.3..0.7: [ count.to_i, :trunc ]
363 when 0.7..1.0: [ count.ceil, :greater ]
367 def _format_time(count, unit_name)
368 number = count < X_TimeWords.length ? X_TimeWords[count-1] : count.to_s
369 unit = unit_name + (count > 1 ? "s" : "")
370 return number + " " + unit
373 def seconds_to_s_fuzzy(seconds)
374 return "zero seconds" if seconds == 0
375 X_TimeUnits.each_index do |i|
376 unit_name, unit_size = X_TimeUnits[i]
378 number_of = seconds.to_f / unit_size
379 next if number_of <= 0.7
381 cnt, action = _round_time(seconds, unit_size)
383 sub_unit_name, size = X_TimeUnits[i+1]
384 sub_cnt = ((seconds % unit_size).to_f / size).round
385 return "#{_format_time cnt, unit_name} and #{_format_time sub_cnt, sub_unit_name}"
387 return "#{X_TimePrefixes[action]}#{_format_time cnt, unit_name}"
392 def seconds_to_s_exact(seconds)
394 m = (seconds /= 60) % 60
395 h = (seconds /= 60) % 24
398 out << "#{d}d" if d > 0
399 out << "#{h}h" if h > 0
400 out << "#{m}m" if m > 0
401 out << "#{s}s" if s > 0
402 out.length > 0 ? out.join(' ') : '0s'
405 def seconds_to_s(seconds, irc = nil)
406 if irc and (u = $user.get_data(irc.from.nick, irc.server.name)) and !(u['fuzzy-time'] == false)
407 seconds_to_s_fuzzy(seconds)
409 seconds_to_s_exact(seconds)