From 34aeec60dc1afb648243313866128bff90158d06 Mon Sep 17 00:00:00 2001 From: Marrub Date: Sat, 23 Feb 2019 23:46:40 -0500 Subject: [PATCH] documentation and version bump --- .yardopts | 2 +- source/common.rb | 42 +++++++----- source/main.rb | 68 +++++++++++++------ source/module.rb | 32 ++++++--- source/modules/base.rb | 84 +++++++++++++++++++++++ source/server.rb | 136 ++++++++++++++------------------------ source/servers/discord.rb | 26 +++++--- 7 files changed, 250 insertions(+), 140 deletions(-) create mode 100644 source/modules/base.rb diff --git a/.yardopts b/.yardopts index 784b87e..7fbee05 100644 --- a/.yardopts +++ b/.yardopts @@ -1 +1 @@ ---protected source/*.rb source/servers/*.rb +--protected --private --hide-void-return source/*.rb source/servers/*.rb diff --git a/source/common.rb b/source/common.rb index 2022056..6c15805 100644 --- a/source/common.rb +++ b/source/common.rb @@ -1,23 +1,30 @@ +require 'zlib' + # Module for the main vrobot4 program and global info. module Vrobot4 # The current program version. - Version = "4.00".freeze + Version = "4.01".freeze - def self.debug=(set) @@debug = set end # Sets the current debug level. - def self.debug ( ) @@debug end # Gets the current debug level. + # Sets the current debug level. + # @param set [Integer] the debug level + # @return [void] + def self.set_debug set + @@debug = set + end # Logs to the console. # - # @param lv [Symbol] - # - If +:DEBUG+, this message will not be printed if the global debug - # level is less than 1. - # - If +:DEBUGV+, this message will not be printed if the global debug - # level is less than 2. + # @param lv [Symbol] the log level + # [+:INFO+] This message will always be printed. + # [+:DEBUG+] This message will not be printed if the global debug level is + # less than 1. + # [+:DEBUGV+] This message will not be printed if the global debug level is + # less than 2. # @return [Boolean] true if the message was printed, false otherwise - def self.log lv, *text + def self.log lv, text if (lv != :DEBUG || @@debug >= 1) && (lv != :DEBUGV || @@debug >= 2) - puts "[" + lv.to_s.ljust(6) + "] " + text.join(?\s) + puts "[#{lv.to_s.ljust 6}] #{text}" true else false @@ -25,13 +32,18 @@ module Vrobot4 end # Checks if the argument +s+ is a numeric string. - # @param str [String] the string to check - def self.is_num? str - /\A[-+]?[0-9]*\.?[0-9]+\Z/ === str + # @param s [String] the string to check + # @return [Boolean] true if +s+ is a numeric string, false otherwise + def self.is_num? s + /\A[-+]?[0-9]*\.?[0-9]+\Z/ === s end - private - @@debug = 0 + # Does a stable hash on +s+. + # @param s [String] the string to hash + # @return [Integer] + def self.hash_str s + Zlib.crc32 s + end end ## EOF diff --git a/source/main.rb b/source/main.rb index 75ef458..c085b18 100644 --- a/source/main.rb +++ b/source/main.rb @@ -6,54 +6,80 @@ require 'yaml' module Vrobot4 private - def self.loadBot botinfo - type = botinfo["type"] - serv = Server::get_server_type(type).new(botinfo) - if botinfo.key? "modules" then for mod in botinfo["modules"] do + # Loads a bot from its configuration +botinfo+. + # @param botinfo [Hash] arbitrary bot information + # ["type"] The type by name of the bot. + # @return [Vrobot4::Server] the loaded bot + def self.load_bot botinfo + type = botinfo["type"] + serv = Server::get_server_type(type).new botinfo + + for mod in botinfo["modules"] do serv.load_mod mod - end end + end serv end - def self.runBots bots - thrds = [] - bots.each {|serv| thrds << Thread.new {serv.connect}} - thrds.each {|th| th.join} + # Runs all bots in +bots+ in separate threads. + # @param bots [Array] all bots to run + # @return [void] + def self.run_bots bots + threads = [] + + bots.each do |server| + threads << Thread.new do + server.connect + end + end + + threads.each do |thread| + thread.join + end end public + # Runs the program. - # @param cfg [IO] configuration file to load + # @param cfg [IO] configuration file to read + # @return [void] def self.main cfg - log :INFO, "vrobot version", Version + Thread.abort_on_exception = true + + log :INFO, "vrobot version #{Version}" begin cfg = YAML.load cfg.read rescue - log :ERROR, "error reading bot config:", $! + log :ERROR, "error reading bot config: #{$!}" return end - Vrobot4.debug = cfg["debug"] if cfg.key? "debug" - cfg["loadmods"].each {|mod| load mod, true} + Vrobot4.set_debug(cfg["debug"] || 0) + + cfg["load"].each do |mod| + load mod, true + end begin bots = [] - cfg["bots"].each {|botinfo| bots << loadBot(botinfo)} - log :DEBUGV, "bots:", bots.to_s + + cfg["bots"].each do |botinfo| + bots << load_bot(botinfo) + end + + log :DEBUGV, "bots: #{bots.to_s}" rescue - log :ERROR, "error loading bot config:", - $!.to_s + ?\n + $!.backtrace.first(3).join(?\n) + err = $!.to_s + ?\n + $!.backtrace.first(3).join(?\n) + log :ERROR, "error loading bot config: #{err}" return end - runBots bots + run_bots bots end end -Thread.abort_on_exception = true -Vrobot4.main open(ARGV[0]) +Vrobot4.main open ARGV[0] ## EOF diff --git a/source/module.rb b/source/module.rb index 7749bd2..30e8c31 100644 --- a/source/module.rb +++ b/source/module.rb @@ -1,6 +1,6 @@ # Module for vrobot4 bot modules. module Vrobot4::Module - # A command function. Holds extra info for i.e. permissions. + # A command function. Holds extra info such as help and permissions. class Command attr_reader :help_str # @return [String] help string for this command @@ -15,6 +15,7 @@ module Vrobot4::Module # Checks if this command is usable in a given context. # @param m [Vrobot4::Server::Message] message for context + # @return [Boolean] true if command is usable, false otherwise def usable_in? m m.user.roles.scan(@roles).any? and @function.owner.usable_in? m end @@ -22,6 +23,7 @@ module Vrobot4::Module # Calls the method attached to this command. # @param m [Vrobot4::Server::Message] message for context # @param argv [String] argument string + # @return [void] def run m, argv @function.call(m, argv) end @@ -31,6 +33,7 @@ module Vrobot4::Module class Module # Checks if this module is usable in a given context. # @param m [Vrobot4::Server::Message] message for context + # @return [Boolean] true if usable, false otherwise def self.usable_in? m role = m.user.roles mprm = m.serv.mprm @@ -45,7 +48,7 @@ module Vrobot4::Module def initialize info @commands = {} @info = info - Vrobot4.log :DEBUG, "initialized", self.to_s + Vrobot4.log :DEBUG, "initialized #{self.to_s}" end # Callback for message receiving. @@ -75,12 +78,16 @@ module Vrobot4::Module # Gets all commands usable in a specified context. # @param m [Vrobot4::Server::Message] message for context - # @return [Hash] a hash of all commands by name + # @return [Hash] + # a hash of all commands by name def cmds m - @commands.select {|name, cmd| cmd.usable_in? m} + @commands.select do |name, cmd| + cmd.usable_in? m + end end protected + # Registers a command into this module. # @param fn [Symbol] name of method to add # @param names [String, Array] name(s) to add this command by @@ -108,7 +115,7 @@ module Vrobot4::Module # @param argv [String, Array] array of arguments or a string # @param req [String] specifier string of required arguments # @param opt [String] specifier string of optional arguments - # @return argv + # @return [String, Array] argv def check_args argv, req, opt = "" if argv.is_a? Array then if argv.length < req.length @@ -137,15 +144,21 @@ module Vrobot4::Module end private + + # Helper function for {#check_args}. + # @param arg [String] the argument to check + # @param i [Integer] which argument this is + # @param req [String] type that the argument should be + # @return [void] def check_arg arg, i, req case req when ?N unless Vrobot4.is_num? arg.strip - raise ArgumentError, "Expected a number for arg " + i.to_s + raise ArgumentError, "Expected a number for arg #{i}" end when ?S # Don't need to check anything here. else - raise ArgumentError, "Invalid argument specifier " + req.to_s + raise ArgumentError, "Invalid argument specifier #{req}" end end end @@ -154,13 +167,14 @@ module Vrobot4::Module # @param type [Class] module type to add # @param server [String] server type this module is restricted to (or none) # @param servflags [String] flags the server must have to use this (or none) + # @return [void] def self.add_module_type type, server: nil, servflags: nil @@module_types[type.type] = { type: type, server: server, servflags: servflags ? /[#{servflags}]/ : nil } - Vrobot4.log :INFO, "added module type:", type.type + Vrobot4.log :INFO, "added module type #{type.type}" end # Gets a module type by name from the global list. @@ -170,7 +184,7 @@ module Vrobot4::Module @@module_types[name] end - private + # The set of all module types. @@module_types = {} end diff --git a/source/modules/base.rb b/source/modules/base.rb new file mode 100644 index 0000000..95a00e5 --- /dev/null +++ b/source/modules/base.rb @@ -0,0 +1,84 @@ +class Mod_Base < Vrobot4::Module::Module + def initialize info + super + register :c_help, "help", "Prints documentation for commands." + register :c_die, "die", "Kills all bot instances.", roles: ?o + register :c_modr, "modr", "Reloads a module.", roles: ?o + register :c_modl, "modl", "Loads a module.", roles: ?o + register :c_modu, "modu", "Unloads a module.", roles: ?o + register :c_dbg, "dbg", "Sets the debug level.", roles: ?o + register :c_info, "info", "Prints context info.", roles: ?o + end + + def c_help m, argv + if argv.empty? + cmds = [] + m.serv.each_mod do |mod| + cmds << mod.cmds(m).keys.join(", ") + end + cmds.delete "" + m.reply "Commands:", cmds.join(", ") + else + m.serv.each_mod do |mod| + if (cmd = mod.cmds(m)[argv]) + return m.reply argv + ":", cmd.help_str + end + end + m.reply "Command not found:", argv + end + end + + def c_die m, argv + m.serv.voice_quit m if m.serv.flags.include? ?A + m.reply \ + ["STATUS: DYING", + "ded", + "proceeding to die", + "bye", + "dedededed", + "Thanks, bye!", + "GOTTAGOBYE", + "the orks insisted upon dying"].sample + exit + end + + def c_modr m, argv + argv = check_args argv.split, "S", "S" + m.serv.drop_mod argv[0] + load argv[1], true if argv.length > 1 + m.serv.load_mod argv[0] + end + + def c_modl m, argv + m.serv.load_mod argv + end + + def c_modu m, argv + m.serv.drop_mod argv + end + + def c_dbg m, argv + check_args argv, "N" + Vrobot4.debug = argv.to_i + end + + def c_info m, argv + m.reply_b <<~_end_ + chan.name: #{m.chan.name} + chan.id: #{m.chan.id} + user.name: #{m.user.name} + user.id: #{m.user.id} + user.roles: #{m.user.roles} + serv.flags: #{m.serv.flags} + serv.id: #{m.serv.id} + serv.mprm: #{m.serv.mprm} + _end_ + end + + def on_command m, cnam, argv + Vrobot4.log :DEBUGV, "command #{cnam.to_s} #{argv}" + super + end +end + +## EOF diff --git a/source/server.rb b/source/server.rb index a570b97..5b381ab 100644 --- a/source/server.rb +++ b/source/server.rb @@ -1,5 +1,7 @@ # Module for vrobot4 server interfaces. module Vrobot4::Server + require "./modules/base.rb" + # Generic user information. May be extended. class User attr_reader :name # @return [String] plaintext name of the user @@ -37,11 +39,15 @@ module Vrobot4::Server end # Sends a message to the originating channel. + # @param args [Array] the text to send + # @return [void] def reply *args @reply.call args.join(?\s) end # Sends a large message to the originating channel. + # @param args [Array] the text to send + # @return [void] def reply_b *args @reply_b.call args.join(?\s) end @@ -61,6 +67,7 @@ module Vrobot4::Server # Loads and initializes a module into the load list. # @param mod [Vrobot4::Module::Module] + # @return [void] def load_mod mod mt = Vrobot4::Module.get_module_type(mod) if mt[:server] and mt[:server] != self.class.type or @@ -72,6 +79,7 @@ module Vrobot4::Server # Drops a module from the load list. # @param mod [Vrobot4::Module::Module] + # @return [void] def drop_mod mod mt = Vrobot4::Module.get_module_type(mod) @modules.each_index do |i| @@ -80,12 +88,16 @@ module Vrobot4::Server end # Yields for every module loaded in the server. + # @yieldparam mod [Vrobot4::Module::Module] an individual module + # @return [void] def each_mod - @modules.each {|mod| yield mod} + @modules.each do |mod| + yield mod + end end # (see Vrobot4::Module::Module#on_message) - # Passes information to all modules. + # This function passes information to all modules. def on_message m @modules.each do |mod| break if mod.class.usable_in? m and mod.on_message m @@ -93,7 +105,7 @@ module Vrobot4::Server end # (see Vrobot4::Module::Module#on_command) - # Passes information to all modules. + # This function passes information to all modules. def on_command m, cnam, argv @modules.each do |mod| break if mod.class.usable_in? m and mod.on_command m, cnam, argv @@ -101,27 +113,27 @@ module Vrobot4::Server end # Connect to the server. + # @abstract + # @return [void] def connect raise NotImplementedError, "Server#connect not implemented" end # Flags for this server. - # [A] Server has audio capabilities and inherits from AudioServer. + # [A] Server has audio capabilities and inherits from + # {Vrobot4::Server::AudioServer}. # [L] Server cannot support large text dumps # (code should assume <=4 lines, 240 characters per as maximum) - # @return [String] + # @return [String] all available flags def flags "" end - # Basic command message handler. - # - # Checks if the message starts with "." and splits the command - # from the arguments, then emits an on_command event. - # - # Otherwise, it will emit an on_message event. - # + # Checks if the message starts with +.+ and splits the command from the + # arguments, then emits an {#on_command} event. Otherwise, it will emit + # an {#on_message} event. # @param m [Vrobot4::Server::Message] the message to parse and emit + # @return [void] def handle_text_cmd m if m.msg.start_with? ?. arg = m.msg.split(?\s, 2) @@ -132,8 +144,12 @@ module Vrobot4::Server end protected + # Implementation defined permission loader. # Loads information into +@mprm+. + # @abstract + # @param pinf [Hash] arbitrary permissions information + # @return [void] def load_permissions pinf raise NotImplementedError, "Server#load_permissions not implemented" end @@ -141,94 +157,44 @@ module Vrobot4::Server # Basis for an audio-enabled server interface. class AudioServer < Server - ## TODO: make API for this - # (see Vrobot4::Server::Server#flags) def flags ?A end - end - class Mod_Base < Vrobot4::Module::Module - def initialize info - super - register :c_help, "help", "Prints documentation for commands." - register :c_die, "die", "Kills all bot instances.", roles: ?o - register :c_modr, "modr", "Reloads a module.", roles: ?o - register :c_modl, "modl", "Loads a module.", roles: ?o - register :c_modu, "modu", "Unloads a module.", roles: ?o - register :c_dbg, "dbg", "Sets the debug level.", roles: ?o + # Quit using voice channels. + # @abstract + # @param m [Vrobot4::Server::Message] message for context + # @return [void] + def voice_quit m + raise NotImplementedError, "AudioServer#voice_quit not implemented" end - # @!visibility private - def c_help m, argv - if argv.empty? - cmds = [] - m.serv.each_mod {|mod| cmds << mod.cmds(m).keys.join(", ")} - cmds.delete "" - m.reply "Commands:", cmds.join(", ") - else - m.serv.each_mod do |mod| - if (cmd = mod.cmds(m)[argv]) - return m.reply argv + ":", cmd.help_str - end - end - m.reply "Command not found:", argv - end + # Plays an audio file by name into the current voice channel. + # @abstract + # @param m [Vrobot4::Server::Message] message for context + # @param fname [String] file name + # @return [void] + def voice_play m, fname + raise NotImplementedError, "AudioServer#voice_play not implemented" end - # @!visibility private - def c_die m, argv - m.serv.voice_quit m if m.serv.flags.include? ?A - m.reply \ - ["STATUS: DYING", - "ded", - "proceeding to die", - "bye", - "dedededed", - "Thanks, bye!", - "GOTTAGOBYE", - "the orks insisted upon dying"].sample - exit - end - - # @!visibility private - def c_modr m, argv - argv = check_args argv.split, "S", "S" - m.serv.drop_mod argv[0] - load argv[1], true if argv.length > 1 - m.serv.load_mod argv[0] - end - - # @!visibility private - def c_modl m, argv - m.serv.load_mod argv - end - - # @!visibility private - def c_modu m, argv - m.serv.drop_mod argv - end - - # @!visibility private - def c_dbg m, argv - check_args argv, "N" - Vrobot4.debug = argv.to_i - end - - # @!visibility private - def on_command m, cnam, argv - Vrobot4.log :DEBUGV, "command", cnam.to_s, argv - super + # Plays an audio I/O stream into the current voice channel. + # @abstract + # @param m [Vrobot4::Server::Message] message for context + # @param io [IO] io stream + # @return [void] + def voice_play_io m, io + raise NotImplementedError, "AudioServer#voice_play_io not implemented" end end - private_constant :Mod_Base # Adds a server type to the global list. # @param type [Class] server type to add + # @return [void] def self.add_server_type type @@server_types[type.type] = type - Vrobot4.log :INFO, "added server type:", type.type + Vrobot4.log :INFO, "added server type: #{type.type}" end # Gets a server type by name from the global list. @@ -238,7 +204,7 @@ module Vrobot4::Server @@server_types[name] end - private + # The set of all server types. @@server_types = {} end diff --git a/source/servers/discord.rb b/source/servers/discord.rb index 1a73c51..483d697 100644 --- a/source/servers/discord.rb +++ b/source/servers/discord.rb @@ -28,30 +28,32 @@ class Sv_Discord < Vrobot4::Server::AudioServer chan: Channel.new(evt.channel), serv: self, reply: -> (text) {evt.respond text}, - reply_b: -> (text) {evt.respond "```\n" + text + "```"} + reply_b: -> (text) {evt.respond "```\n#{text}```"} handle_text_cmd m end end - # (see Vrobot4::Server::AudioServer#voice_join) + # (see Vrobot4::Server::AudioServer#voice_quit) def voice_quit m @bot.voice_destroy m.chan.real.server.id end # (see Vrobot4::Server::AudioServer#voice_play) def voice_play m, fname - vc = @bot.voice(m.chan.real) + vc = @bot.voice m.chan.real raise RuntimeError, "I'm not in a voice channel" unless vc vc.play_file fname end # (see Vrobot4::Server::AudioServer#voice_play_io) def voice_play_io m, io - vc = @bot.voice(m.chan.real) - raise ArgumentError, "Invalid i/o stream" unless io + vc = @bot.voice m.chan.real + raise ArgumentError, "Invalid IO stream" unless io raise RuntimeError, "I'm not in a voice channel" unless vc - Thread.new {vc.play_io io} + Thread.new do + vc.play_io io + end # HACK sleep 1 oldst = nil @@ -72,6 +74,7 @@ class Sv_Discord < Vrobot4::Server::AudioServer end protected + # (see Vrobot4::Server::Server#load_permissions) def load_permissions pinf @mprm = {chan: {}, role: {}, glob: {}} @@ -88,13 +91,18 @@ class Sv_Discord < Vrobot4::Server::AudioServer end if pinf end + # Helper for channel permissions handling. class ChannelPerms def initialize() @cprm = {} end - # Returns true if the channel is enabled, false otherwise. + # @param chan [Sv_Discord::Channel] the channel for this permission + # @return [Boolean] true if the channel is enabled, false otherwise. def [](chan) @cprm[chan.name] or @cprm[chan.real.id] end # Sets a channel's permission on/off. + # @param chan [Sv_Discord::Channel] the channel for this permission + # @param set [Boolean] if this permission should be set + # @return [void] def []=(chan, set) @cprm[chan] = set end end private_constant :ChannelPerms @@ -114,9 +122,9 @@ class Sv_Discord < Vrobot4::Server::AudioServer if user.is_a? Discordrb::Member if user.owner? @roles += "Ooh" - elsif ops and ops.any? {|role| user.role? role} + elsif ops and ops.any? do |role| user.role? role end @roles += "oh" - elsif hop and hop.any? {|role| user.role? role} + elsif hop and hop.any? do |role| user.role? role end @roles += "h" end end