diff --git a/core/bot.lua b/core/bot.lua index 9cd5831..a3a2f9b 100644 --- a/core/bot.lua +++ b/core/bot.lua @@ -20,7 +20,6 @@ end function bot:OnChannelMessage(Username, Channel, Message) if Message:sub(1,#self._prefix) == self._prefix then local String = Message:sub(#self._prefix + 1) - print(String) local Command local Arguments = {} for Part in String:gmatch("%S+") do @@ -35,11 +34,33 @@ function bot:OnChannelMessage(Username, Channel, Message) Channel:Say("Unknown command '" .. Command .. "'.") else local CommandData = self._commands[Command] - if #Arguments ~= CommandData.Arguments then + if #Arguments ~= CommandData.Arguments and CommandData.Arguments ~= -1 then Channel:Say(string.format("Command '%s' expects '%i' arguments, got '%i'.", Command, CommandData.Arguments, #Arguments)) else - CommandData.Callback(Username, Channel, unpack(Arguments)) + -- -1 means we want a raw string + if CommandData.Arguments == -1 then + if #Arguments < 1 then + Channel:Say("Please provide an argument.") + return + end + Arguments = { table.concat(Arguments, ' ') } + end + local RequiredAccess = CommandData.Access + if RequiredAcess == 0 then + CommandData.Callback(Username, Channel, unpack(Arguments)) + return + end + local UserAccess = hook.Call("auth.GetLevel", Channel, Username) + if not UserAccess then + Channel:Say("Could not run command because auth backend is missing.") + return + end + if UserAccess >= RequiredAccess then + CommandData.Callback(Username, Channel, unpack(Arguments)) + else + Channel:Say(string.format("Unsufficient access level (%i required).", RequiredAccess)) + end end end end diff --git a/core/config.lua b/core/config.lua new file mode 100644 index 0000000..39def3c --- /dev/null +++ b/core/config.lua @@ -0,0 +1,40 @@ +config = {} + +function config:Load(filename) + local File, Message = io.open(filename, "r") + if not File then + error("Could not open config file: " .. Message) + end + + self.Sections = {} + local CurrentSection = "Default" + self.Sections[CurrentSection] = {} + + local Data = File:read('*a') + Data:gsub("(.-)\r?\n", function(line) + local Line = line:gsub("(#.*)", "") + Line = Line:gsub("^%s*(.-)%s*$", "%1") + if Line:len() == 0 then + return + end + local Section = Line:match("%[([a-zA-Z0-9%-]+)%]") + if Section ~= nil then + CurrentSection = Section + self.Sections[CurrentSection] = {} + else + local Key, Value = Line:match("([^= ]+) *= *([^=]+)") + print(Key,Value) + if Key ~= nil and Value ~= nil then + self.Sections[CurrentSection][Key] = Value + end + end + end) +end + + +function config:Get(Section, Key) + if self.Sections[Section] == nil then + return nil + end + return self.Sections[Section][Key] +end diff --git a/core/plugin.lua b/core/plugin.lua index 67e012b..938e12e 100644 --- a/core/plugin.lua +++ b/core/plugin.lua @@ -4,6 +4,8 @@ -- The goal is to be able to unload a plugin and remove all its' hooks -- and commands. +require('lfs') + local Plugins = {} plugin = {} @@ -19,10 +21,10 @@ function API.AddHook(plugin_id, event_name, hook_name, callback) HookInfo.EventName = event_name HookInfo.HookName = HookName - PluginHooks[#PluginHooks + 1] = Info + PluginHooks[#PluginHooks + 1] = HookInfo hook.Add(event_name, HookName, function(...) local Args = {...} - local Success, Message = pcall(callback(unpack(Args))) + local Success, Message = pcall(callback, unpack(Args)) if not Success then hook.Call("plugin.HookCallFailed", HookName, Message) return nil @@ -33,7 +35,10 @@ function API.AddHook(plugin_id, event_name, hook_name, callback) end function API.AddCommand(plugin_id, command_name, arity, callback, access, help) - bot:AddCommand(command_name, arity, callback, access, help) + bot:AddCommand(command_name, arity, function(...) + plugin.Quota(5) + return callback(...) + end, access, help) local PluginCommands = Plugins[plugin_id].Commands PluginCommands[#PluginCommands + 1] = command_name end @@ -51,6 +56,20 @@ function API.CurrentTime(plugin_id) return os.time() end +function API.ConfigGet(plugin_id, Key) + return config:Get("plugin-" .. plugin_id, Key) +end + +function plugin.Quota(seconds) + local Start = os.time() + debug.sethook(function() + if os.time() - Start > seconds then + debug.sethook() + error("Time quota exceeded.") + end + end, "", 100000) +end + function plugin.Create(plugin_id) local Plugin = {} Plugin.Hooks = {} @@ -97,6 +116,22 @@ function plugin.PrepareEnvironment(plugin_id) Env.table = require('table') Env.string = require('string') Env.json = require('json') + Env.DBI = require('DBI') + Env.print = print + Env.error = error + Env.tonumber = tonumber + Env.tostring = tostring + Env.pcall = pcall + Env.loadstring = function(s) + if s:byte(1) == 27 then + return nil, "Refusing to load bytecode" + else + return loadstring(s) + end + end + Env.setfenv = setfenv + Env.pairs = pairs + Env._G = Env Env.plugin = {} for K, F in pairs(API) do @@ -156,3 +191,25 @@ function plugin.AddRuntimeCommands() end end, "Unload a previously loaded plugin.", 100) end + +function plugin.Discover() + for Filename in lfs.dir('plugins/') do + local FullFilename = 'plugins/' .. Filename + local Attributes = lfs.attributes(FullFilename) + if Attributes.mode == 'file' and FullFilename:sub(-4) == '.lua' then + local PluginName = Filename:sub(1, -5) + hook.Call('info', 'Loading plugin ' .. PluginName) + + local File, Message = io.open(FullFilename) + if not File then + hook.Call('info', 'Skipping: ' .. Message) + else + local Data = File:read('*a') + local Success, Message = plugin.RunCode(PluginName, Data) + if not Success then + error(string.format("Could not load plugin %s: %s.", PluginName, Message)) + end + end + end + end +end diff --git a/moonspeak.ini.dist b/moonspeak.ini.dist new file mode 100644 index 0000000..0215986 --- /dev/null +++ b/moonspeak.ini.dist @@ -0,0 +1,13 @@ +[irc] +server = irc.freenode.net +port = 6667 +nickname = moonspeak +username = moonspeak +realname = moonspeak + +[plugin-auth-postgres] +server = 1.1.1.1 +username = user +database = db +password = pass + diff --git a/plugins/auth-postgres.lua b/plugins/auth-postgres.lua new file mode 100644 index 0000000..7393211 --- /dev/null +++ b/plugins/auth-postgres.lua @@ -0,0 +1,29 @@ +postgres = {} + +local function check_connection() + if not postgres.db or not postgres.db:ping() then + local Server = plugin.ConfigGet('server') + local Username = plugin.ConfigGet('username') + local Password = plugin.ConfigGet('password') + local Database = plugin.ConfigGet('database') + local Port = tonumber(plugin.ConfigGet('port')) or 5432 + postgres.db = DBI.Connect('PostgreSQL', Database, Username, Password, Server, Port) + end + if not postgres.db then + error("Could not connect to the PostgreSQL database!") + return false + end + return true +end + +plugin.AddHook('auth.GetLevel', 'GetLevel', function(Channel, Account) + if check_connection() then + local Statement = postgres.db:prepare("select _level from _level where _account = ? and _channel = ?") + print(Account, Channel.Name) + Statement:execute(Account, Channel.Name) + for Row in Statement:rows(true) do + return tonumber(Row._level) + end + return 0 + end +end) diff --git a/plugins/repl.lua b/plugins/repl.lua new file mode 100644 index 0000000..637d225 --- /dev/null +++ b/plugins/repl.lua @@ -0,0 +1,24 @@ +plugin.AddCommand('eval', -1, function(User, Channel, String) + local Function, Message = loadstring(String) + if not Function then + Channel:Say("Parse error: " .. Message) + return + end + local Env = {} + for K, V in pairs(_G) do + Env[K] = V + end + Env.plugin = nil + Env.loadstring = nil + Env.pcall = nil + Env.setfenv = nil + Env._G = Env + + setfenv(Function, Env) + local Result, Message = pcall(Function) + if Result then + Channel:Say("OK -> " .. tostring(Message)) + else + Channel:Say("Error -> " .. tostring(Message)) + end +end, "Runs a Lua command in a sandbox.", 10) diff --git a/start.lua b/start.lua index cbde3a5..8dc59bb 100644 --- a/start.lua +++ b/start.lua @@ -1,13 +1,10 @@ require('core.hook') +require('core.config') require('core.reactor') require('core.irc') require('core.bot') require('core.plugin') -require('socket') -local https = require('ssl.https') -require('json') - hook.Add('info', 'repl-info', function(Message) print('INFO: ' .. Message) end) @@ -16,26 +13,24 @@ hook.Add('debug', 'repl-debug', function(Message) print('DEBUG: ' .. Message) end) +hook.Add('plugin.HookCallFailed', 'repl-debug', function(Name, Message) + print(string.format("Plugin hook call failed! %s: %s", Name, Message)) +end) + hook.Add('irc.Connected', 'repl-connected', function() irc:Join('#hackerspace-pl-bottest') end) +config:Load('moonspeak.ini') +local Server = config:Get('irc', 'server') +local Port = tonumber(config:Get('irc', 'port')) or 6667 +local Nickname = config:Get('irc', 'nickname') +local Username = config:Get('irc', 'username') +local Realname = config:Get('irc', 'realname') + reactor:Initialize() bot:Initialize(irc, ',') plugin.AddRuntimeCommands() ---[[bot:AddCommand('at', 0, function(Username, Channel) - local Body, Code, Headers, Status = https.request('https://at.hackerspace.pl/api') - if Code ~= 200 then - error(string.format("Status code returned: %i", Code)) - end - local Data = json.decode.decode(Body) - local Users = {} - for K, User in pairs(Data.users) do - Users[#Users + 1] = User.login - end - Channel:Say(table.concat(Users, ',')) - -end, "Show who's at the Warsaw Hackerspace.")]]-- - -irc:Connect('irc.freenode.net', 6667, 'moonspeak', 'moonspeak', 'moonspeak') +plugin.Discover() +irc:Connect(Server, Port, Nickname, Username, Realname) reactor:Run()