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 path = "data/#{self.class.name.downcase}"
168 Dir.mkdir(path) unless File.directory?(path)
169 File.open("#{path}/#{name}", mode, &block)
172 # Load/save plugin-state. Override to do anything here.
173 # These are called on init and before the bot quits.
179 # Command help generator.
182 hm = @help_map || (@help_map = {})
185 attr_accessor :help_map
188 # Command parser wrapper generator.
191 # channel A channel the bot is on, or automatically
192 # set to the channel the command is called from.
193 # anychannel Arbitrary channel name.
194 # integer An integer.
195 # text Any text, possibly empty. Must be last argument.
196 # text_ne Any (non-empty) text. Ditto.
198 def PluginBase.parse(name, *args)
200 # No args, nothing to do.
201 return unless args.length > 0
204 new_name = '__parse_' + name.to_s
205 alias_method(new_name.to_sym, name.to_sym)
207 # And put in our new one.
209 def #{name.to_s}(irc, line)
210 #{new_name}(irc, *parse(irc, line, #{args.map {|a| a.kind_of?(String) ?
211 ('"' + a + '"') : (':' + a.to_s)}.join(', ')}))
214 # Kernel.puts "Genrated code: " + code
219 # Argument parse error.
220 class ParseError < Exception
221 def initialize(msg, arg_num, usage, format)
222 super("Argument #{arg_num + 1}: #{msg}")
227 @usage ? @usage : (@format.map { |f|
229 when :nick: '<nick name>'
230 when :channel: '[bot channel]'
231 when :anychannel: '<channel>'
232 when :integer: '<integer>'
233 when :text: '[string]'
234 when :text_ne: '<string>'
241 def parse(irc, line, *forms)
242 usage = forms[0].kind_of?(String) ? forms.delete_at(0) : nil
245 forms.length.times do |i|
248 # Text. Last argument.
249 if f == :text or f == :text_ne
250 line = line.strip if line
251 if f == :text_ne and (!line or line.length == 0)
252 raise ParseError.new('Non-empty string expected.', i, usage, forms)
254 args << (line ? line : '')
257 # Current- or bot-channel.
262 if line: word, line = line.split(' ', 2)
264 unless word and (c = irc.server.channels[word])
265 raise ParseError.new('Bot-channel name expected.', i, usage, forms)
272 if line: word, line = line.split(' ', 2)
274 unless word and word.length > 0
275 raise ParseError.new('Not enough arguments.', i, usage, forms)
285 unless word and word.length > 0 and (word[0] == ?# or word[0] == ?&)
286 raise ParseError.new('Channel name expected.', i, usage, forms)
296 raise ParseError.new('Integer expected.', i, usage, forms)
307 WeekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
309 days, now = ts.to_i / (24*60*60), Time.now.to_i / (24*60*60)
310 if days == now: "at #{ts.strftime('%H:%M')}"
311 elsif days + 1 == now: "yesterday at #{ts.strftime('%H:%M')}"
312 elsif days + 6 <= now: "#{WeekDays[ts.wday]} at #{ts.strftime('%H:%M')}"
313 else "on #{ts.strftime('%m-%d %H:%M')}"
317 # Evaluate a setting (or settings) on channel and server levels, with optional
318 # global level provided by the passed block.
319 def setting_cs(irc, name = nil)
321 if (s = $config["servers/#{irc.server.name}"])
322 if (c = s['channels']) and (c = c[irc.channel.name])
323 a = name ? c[name] : yield(c)
326 a = name ? s[name] : yield(s)
329 if a.nil? and block_given?
330 a = name ? yield : yield(nil)
336 # ---------------------------------------------------------------------------
337 # Time duration formatter.
338 # ---------------------------------------------------------------------------
340 X_TimeWords = %w{ one two three four five six seven eight nine ten eleven twelve } # thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty
343 [ "year", 365 * 60 * 60 * 24 ],
344 [ "month", 30.4 * 60 * 60 * 24 ],
345 [ "week", 7 * 60 * 60 * 24 ],
346 [ "day", 24 * 60 * 60 ],
352 X_TimePrefixes = { :greater => "almost ", :less => "about " }
354 def _round_time(seconds, unit_size)
355 count = seconds.to_f / unit_size
357 when 0.0: [ count.to_i, :exact ]
358 when 0.0..0.3: [ count.floor, :less ]
359 when 0.3..0.7: [ count.to_i, :trunc ]
360 when 0.7..1.0: [ count.ceil, :greater ]
364 def _format_time(count, unit_name)
365 number = count < X_TimeWords.length ? X_TimeWords[count-1] : count.to_s
366 unit = unit_name + (count > 1 ? "s" : "")
367 return number + " " + unit
370 def seconds_to_s_fuzzy(seconds)
371 return "zero seconds" if seconds == 0
372 X_TimeUnits.each_index do |i|
373 unit_name, unit_size = X_TimeUnits[i]
375 number_of = seconds.to_f / unit_size
376 next if number_of <= 0.7
378 cnt, action = _round_time(seconds, unit_size)
380 sub_unit_name, size = X_TimeUnits[i+1]
381 sub_cnt = ((seconds % unit_size).to_f / size).round
382 return "#{_format_time cnt, unit_name} and #{_format_time sub_cnt, sub_unit_name}"
384 return "#{X_TimePrefixes[action]}#{_format_time cnt, unit_name}"
389 def seconds_to_s_exact(seconds)
391 m = (seconds /= 60) % 60
392 h = (seconds /= 60) % 24
395 out << "#{d}d" if d > 0
396 out << "#{h}h" if h > 0
397 out << "#{m}m" if m > 0
398 out << "#{s}s" if s > 0
399 out.length > 0 ? out.join(' ') : '0s'
402 def seconds_to_s(seconds, irc = nil)
403 if irc and (u = $user.get_data(irc.from.nick, irc.server.name)) and !(u['fuzzy-time'] == false)
404 seconds_to_s_fuzzy(seconds)
406 seconds_to_s_exact(seconds)