Correcting spelling error (to get the hang of git)
[six.git] / pluginbase.rb
blob6b181097db1b79e148f97616d50548abe44da211
3 # CyBot plugin root class.
6 class PluginBase
8   # Command flags.
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
17   end
19   # Force a new instance to be created. Used when loading.
20   def PluginBase.instance
21     @instance = self.new
22   end
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
29   #         load, as usual.
31   def before_reload(forced = false)
32     nil
33   end
34   def after_reload(forced = false)
35     false
36   end
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)
41   end
43   # Access to stuff.
44   attr_reader :commands, :hooks, :brief_help
45   attr_accessor :sensitive_command
47   # Register a 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]]
50     @commands[name] = cmd
51     if (c = $commands[name])
52       if c[0] != CmdFlag_Multiple
53         $commands[name] = (c = [CmdFlag_Multiple, c])
54       end
55       c << cmd
56     elsif (p = $plugins[name])
57       $commands[name] = [CmdFlag_Multiple, cmd]
58     else
59       $commands[name] = cmd
60     end
61   end
63   # Register commands for the given plugin instance.
64   def register_commands
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_(.*)$/
77         hook = $1.to_sym
78         h = [method(name.to_sym), hook, self]
79         hook_list = $hooks[hook] || ($hooks[hook] = [])
80         @hooks << h
81         hook_list << h
82       end
83     end
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]
88     end
90     # Step 3: Clean up.
91     self.class.help_map = nil
93   end
95   # We need to be able to call this.
96   class << self
97     public :remove_method
98   end
100   # Unregister commands and hooks for this plugin. Override to do dispose stuff.
101   def unregister
103     # Commands...
104     klass = self.class
105     @commands.each do |k,v|
107       # Remove method.
108       name = case v[0]
109         when CmdFlag_Normal:  'cmd_'
110         when CmdFlag_Channel: 'chan_'
111         when CmdFlag_Server:  'serv_'
112       end + k.to_s
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
119         else
120           j = cmd.each_with_index do |c, i|
121             next unless i > 0
122             break i if c[2].class == klass
123           end
124           cmd.delete_at(j) if j != cmd
125         end
126       end
128     end
130     # Hooks...
131     @hooks.each do |h|
132       klass.remove_method "hook_#{h[1]}".to_sym
133       if (hook = $hooks[h[1]])
134         hook.delete_if { |e| e[2].class == klass }
135       end
136     end
138     # Clean up.
139     @commands = {}
140     @hooks = []
142   end
144   # Only override this if you know what you're doing.
145   def name
146     @name ||= self.class.name.downcase
147   end
149   # Remember to call this to get your commands regged.
150   def initialize
151     @commands = {}
152     @hooks = []
153     @brief_help = "#{name.capitalize} plugin." unless @brief_help
154     register_commands
155     load
156   end
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] != ?/
162     "#{dir}#{name}"
163   end
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)
170   end
172   # Load/save plugin-state. Override to do anything here.
173   # These are called on init and before the bot quits.
174   def load
175   end
176   def save
177   end
179   # Command help generator.
180   class << self
181     def help(cmd, help)
182       hm = @help_map || (@help_map = {})
183       hm[cmd] = help
184     end
185     attr_accessor :help_map
186   end
188   # Command parser wrapper generator.
189   # Argument types:
190   #   nick          A nick name.
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.
197   #
198   def PluginBase.parse(name, *args)
200     # No args, nothing to do.
201     return unless args.length > 0
203     # Alias old method.
204     new_name = '__parse_' + name.to_s
205     alias_method(new_name.to_sym, name.to_sym)
207     # And put in our new one.
208     code = <<-EOS
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(', ')}))
212       end
213     EOS
214 #    Kernel.puts "Genrated code: " + code
215     class_eval code
217   end
219   # Argument parse error.
220   class ParseError < Exception
221     def initialize(msg, arg_num, usage, format)
222       super("Argument #{arg_num + 1}: #{msg}")
223       @usage  = usage
224       @format = format
225     end
226     def usage
227       @usage ? @usage : (@format.map { |f|
228          case 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>'
235          end
236       }.join(' '))
237     end
238   end
240   # Argument parser.
241   def parse(irc, line, *forms)
242     usage = forms[0].kind_of?(String) ? forms.delete_at(0) : nil
243     args = []
244     line ||= ''
245     forms.length.times do |i|
246       f = forms[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)
253         end
254         args << (line ? line : '')
255         break
257       # Current- or bot-channel.
258       elsif f == :channel
259         if (c = irc.channel)
260           args << c
261         else
262           if line: word, line = line.split(' ', 2)
263           else word = nil end
264           unless word and (c = irc.server.channels[word])
265             raise ParseError.new('Bot-channel name expected.', i, usage, forms)
266           end
267           args << c
268         end
270       # Other tokens.
271       else
272         if line: word, line = line.split(' ', 2)
273         else word = nil end
274         unless word and word.length > 0
275           raise ParseError.new('Not enough arguments.', i, usage, forms)
276         end
277         case f
279         # Nick name.  FIXME?
280         when :nick
281           args << word
283         # Channel name.
284         when :anychannel
285           unless word and word.length > 0 and (word[0] == ?# or word[0] == ?&)
286             raise ParseError.new('Channel name expected.', i, usage, forms)
287           end
288           args << word
290         # Integer.
291         when :integer
292           begin
293             int = Integer(word)
294             args << int
295           rescue ArgumentError
296             raise ParseError.new('Integer expected.', i, usage, forms)
297           end
299         end
300       end
302     end
303     args
304   end
306   # Date formatter.
307   WeekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
308   def human_date(ts)
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')}"
314     end
315   end
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)
320     a = 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)
324       end
325       if a.nil?
326         a = name ? s[name] : yield(s)
327       end
328     end
329     if a.nil? and block_given?
330       a = name ? yield : yield(nil)
331     end
332     a
333   end
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
342   X_TimeUnits = [
343     [ "year",    365   * 60 * 60 * 24 ],
344     [ "month",    30.4 * 60 * 60 * 24 ],
345     [ "week",      7   * 60 * 60 * 24 ],
346     [ "day",      24   * 60 * 60      ],
347     [ "hour",     60   * 60           ],
348     [ "minute",   60                  ],
349     [ "second",    1                  ],
350   ]
352   X_TimePrefixes = { :greater => "almost ", :less => "about " }
354   def _round_time(seconds, unit_size)
355     count = seconds.to_f / unit_size
356     case count % 1.0
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 ]
361     end
362   end
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
368   end
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)
379       if action == :trunc
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}"
383       else
384         return "#{X_TimePrefixes[action]}#{_format_time cnt, unit_name}"
385       end
386     end
387   end
389   def seconds_to_s_exact(seconds)
390     s = seconds % 60
391     m = (seconds /= 60) % 60
392     h = (seconds /= 60) % 24
393     d = (seconds /= 24)
394     out = []
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'
400   end
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)
405     else
406       seconds_to_s_exact(seconds)
407     end
408   end