forestbot/plugins/help.rb

207 lines
6.9 KiB
Ruby
Raw Normal View History

# -*- coding: utf-8 -*-
#
# = Cinch help plugin
# This plugin parses the +help+ option of all plugins registered with
# the bot when connecting to the server and provides a nice interface
# for querying these help strings via the 'help' command.
#
# == Commands
# [cinch help]
# Print the intro message and list all plugins.
# [cinch help <plugin>]
# List all commands with explanations for a plugin.
# [cinch help search <query>]
# List all commands with explanations that contain <query>.
#
# == Dependencies
# None.
#
# == Configuration
# Add the following to your bots configure.do stanza:
#
# config.plugins.options[Cinch::Help] = {
# :intro => "%s at your service."
# }
#
# [intro]
# First message posted when the user issues 'help' to the bot
# without any parameters. %s is replaced with Cinchs current
# nickname.
#
# == Writing help messages
# In order to add help strings, update your plugins to set +help+
# properly like this:
#
# class YourPlugin
# include Cinch::Plugin
#
# set :help, <<-HELP
# command <para1> <para2>
# This command does this and that. The description may
# as well be multiline.
# command2 <para1> [optpara]
# Another sample command.
# HELP
# end
#
# Cinch recognises a command in the help string as one if it starts at
# the beginning of the line, all subsequent indented lines are considered
# part of the command explanation, up until the next line that is not
# indented or the end of the string. Multiline explanations are therefore
# handled fine, but you should still try to keep the descriptions as short
# as possible, because many IRC servers will block too much text as flooding,
# resulting in the help messages being cut. Instead, provide a web link or
# something similar for full-blown descriptions.
#
# The command itself may be in any form you want (as long as its a single
# line), but I recommend the following conventions so users know how to
# talk to the bot:
#
# [cinch ...]
# A command Cinch understands when posted publicely into the channel,
# prefixed with its name (or whatever prefix you want, replace "cinch"
# accordingly).
# [/msg cinch ...]
# A command Cinch understands when send directly to him via PM and
# without a prefix.
# [[/msg] cinch ...]
# A command Cinch understands when posted publicely with his nick as
# a prefix, or privately to him without any prefix.
# [...]
# That is, a bare command without a prefix. Cinch watches the conversion
# on the channel, and whenever he encounters this string/pattern he will
# take action, without any prefix at all.
#
# The word "cinch" in the command string will automatically be replaced with
# the actual nickname of your bot.
#
# == Author
# Marvin Gülker (Quintus)
#
# == License
# A help plugin for Cinch.
# Copyright © 2012 Marvin Gülker
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Help plugin for Cinch.
class Cinch::Help
include Cinch::Plugin
listen_to :connect, :method => :on_connect
match /help(.*)/i, :prefix => lambda{|msg| Regexp.compile("^#{Regexp.escape(msg.bot.nick)}:?\s*")}, :react_on => :channel
match /help(.*)/i, :use_prefix => false, :react_on => :private
set :help, <<-EOF
[/msg] cinch help
Post a short introduction and list available plugins.
[/msg] cinch help <plugin>
List all commands available in a plugin.
[/msg] cinch help search <query>
Search all plugins commands and list all commands containing
<query>.
EOF
def execute(msg, query)
query = query.strip.downcase
response = ""
# Act depending on the subcommand.
if query.empty?
response << @intro_message.strip << "\n"
response << "Available plugins:\n"
response << bot.config.plugins.plugins.map{|plugin| format_plugin_name(plugin)}.join(", ")
response << "\n'help <plugin>' for help on a specific plugin."
# Help for a specific plugin
elsif plugin = @help.keys.find{|plugin| format_plugin_name(plugin) == query}
@help[plugin].keys.sort.each do |command|
response << format_command(command, @help[plugin][command], plugin)
end
# help search <...>
elsif query =~ /^search (.*)$/i
query2 = $1.strip
@help.each_pair do |plugin, hsh|
hsh.each_pair do |command, explanation|
response << format_command(command, explanation, plugin) if command.include?(query2)
end
end
# For plugins without help
response << "Sorry, no help available for the #{format_plugin_name(plugin)} plugin." if response.empty?
# Something we don't know what do do with
else
response << "Sorry, I cannot find '#{query}'."
end
response << "Sorry, nothing found." if response.empty?
msg.reply(response)
end
# Called on startup. This method iterates the list of registered plugins
# and parses all their help messages, collecting them in the @help hash,
# which has a structure like this:
#
# {Plugin => {"command" => "explanation"}}
#
# where +Plugin+ is the plugins class object. It also parses configuration
# options.
def on_connect(msg)
@help = {}
if config[:intro]
@intro_message = config[:intro] % bot.nick
else
@intro_message = "#{bot.nick} at your service."
end
bot.config.plugins.plugins.each do |plugin|
@help[plugin] = Hash.new{|h, k| h[k] = ""}
next unless plugin.help # Some plugins don't provide help
current_command = "<unparsable content>" # For not properly formatted help strings
plugin.help.lines.each do |line|
if line =~ /^\s+/
@help[plugin][current_command] << line.strip
else
current_command = line.strip.gsub(/cinch/i, bot.name)
end
end
end
end
private
# Format the help for a single command in a nice, unicode mannor.
def format_command(command, explanation, plugin)
result = ""
result << "┌─ " << command << " ─── Plugin: " << format_plugin_name(plugin) << "" << "\n"
result << explanation.lines.map(&:strip).join(" ").chars.each_slice(80).map(&:join).join("\n")
result << "\n" << "└ ─ ─ ─ ─ ─ ─ ─ ─\n"
result
end
# Downcase the plugin name and clip it to the last component
# of the namespace, so it can be displayed in a user-friendly
# way.
def format_plugin_name(plugin)
plugin.to_s.split("::").last.downcase
end
end