Fixed sandbox, added freenode ID, added coroutines.
parent
7062c3a7ff
commit
9e12656a18
20
core/bot.lua
20
core/bot.lua
|
@ -15,6 +15,19 @@ function bot:Initialize(IRC, Prefix)
|
|||
Channel:Say("Whoops! Error when executing OnChannelMessage: " .. Error)
|
||||
end
|
||||
end)
|
||||
self:AddCommand('eval-core', -1, function(Username, Channel, String)
|
||||
local Function, Message = loadstring(String)
|
||||
if not Function then
|
||||
Channel:Say("Parse error: " .. Message)
|
||||
return
|
||||
end
|
||||
local Status, Message = pcall(Function)
|
||||
if not Status then
|
||||
Channel:Say("Error -> " .. Message)
|
||||
else
|
||||
Channel:Say("OK -> " .. tostring(Message))
|
||||
end
|
||||
end, "Runs a Lua command in the bot context.", 100)
|
||||
end
|
||||
|
||||
function bot:OnChannelMessage(Username, Channel, Message)
|
||||
|
@ -51,7 +64,12 @@ function bot:OnChannelMessage(Username, Channel, Message)
|
|||
CommandData.Callback(Username, Channel, unpack(Arguments))
|
||||
return
|
||||
end
|
||||
local UserAccess = hook.Call("auth.GetLevel", Channel, Username)
|
||||
local Account = hook.Call("auth.GetAccount", irc, Username)
|
||||
if not Account then
|
||||
Channel:Say("Please identify with NickServ.")
|
||||
return
|
||||
end
|
||||
local UserAccess = hook.Call("auth.GetLevel", Channel, Account)
|
||||
if not UserAccess then
|
||||
Channel:Say("Could not run command because auth backend is missing.")
|
||||
return
|
||||
|
|
35
core/irc.lua
35
core/irc.lua
|
@ -7,14 +7,12 @@ function irc.Channel:Say(Message)
|
|||
self._irc:Say(self.Name, Message)
|
||||
end
|
||||
|
||||
function irc:ReceiveData(socket)
|
||||
-- Two seconds look okay for receiving a rest of line
|
||||
socket:settimeout(2)
|
||||
local Data, Error = socket:receive('*l')
|
||||
if Error then
|
||||
error('Could not receive IRC line: ' .. Error)
|
||||
end
|
||||
hook.Call('debug', Data)
|
||||
function irc.Channel:Whois(Nickname)
|
||||
self._irc:Whois(Nickname)
|
||||
end
|
||||
|
||||
function irc:ReceiveData(Data)
|
||||
--hook.Call('debug', Data)
|
||||
local Prefix, Command, Arguments
|
||||
if Data:sub(1, 1) == ':' then
|
||||
local Pattern = ':([^ ]+) +([^ ]+) *(.*)'
|
||||
|
@ -88,6 +86,7 @@ function irc:HandleCommand(Prefix, Command, Arguments)
|
|||
end
|
||||
elseif Number and (Number >= 300) and (Number < 400) then
|
||||
-- IRC server response. some parts of us might be looking for these
|
||||
hook.Call('irc.GetResponse' .. tostring(Number), unpack(Arguments))
|
||||
if self._response_hooks[Number] ~= nil then
|
||||
for K, Callback in pairs(self._response_hooks[Number]) do
|
||||
if Callback(unpack(Arguments)) ~= false then
|
||||
|
@ -112,9 +111,22 @@ function irc:OnResponse(Response, Callback)
|
|||
end
|
||||
|
||||
function irc:_Send(message)
|
||||
local Delay = (self._last_sent + self._send_delay) - os.time()
|
||||
if Delay > 0 then
|
||||
local Function = debug.gethook()
|
||||
if Function ~= nil then
|
||||
Function()
|
||||
end
|
||||
reactor:Sleep(Delay)
|
||||
end
|
||||
self._last_sent = os.time()
|
||||
self.Socket:send(message..'\r\n')
|
||||
end
|
||||
|
||||
function irc:Whois(nickname)
|
||||
self:_Send('WHOIS ' .. nickname)
|
||||
end
|
||||
|
||||
function irc:SetNick(nickname)
|
||||
self:_Send('NICK '..nickname)
|
||||
hook.Call('info', 'Changing nickname to ' .. self._nickname)
|
||||
|
@ -126,7 +138,9 @@ function irc:LoginUser(username, realname)
|
|||
end
|
||||
|
||||
function irc:Say(target, message)
|
||||
self:_Send('PRIVMSG ' .. target .. ' :' .. message)
|
||||
for Line in message:gmatch("[^\r\n]+") do
|
||||
self:_Send('PRIVMSG ' .. target .. ' :' .. Line)
|
||||
end
|
||||
end
|
||||
|
||||
function irc:Join(channel)
|
||||
|
@ -157,7 +171,6 @@ function irc:Join(channel)
|
|||
return false
|
||||
end
|
||||
for Member in Members:gmatch("%S+") do
|
||||
print(Member)
|
||||
local Flag, Name = Member:match('([~@%&]?)(.+)')
|
||||
local Data = {}
|
||||
Data.Name = Name
|
||||
|
@ -179,6 +192,8 @@ function irc:Connect(server, port, nickname, username, realname)
|
|||
|
||||
self._channels = {}
|
||||
self._response_hooks = {}
|
||||
self._last_sent = os.time()
|
||||
self._send_delay = 0.5
|
||||
|
||||
-- Connection procedure (callback hell!)
|
||||
local FinishedInitialNotices = function()
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
|
||||
require('lfs')
|
||||
|
||||
local Plugins = {}
|
||||
|
||||
plugin = {}
|
||||
local API = {}
|
||||
plugin.Plugins = {}
|
||||
plugin.API = {}
|
||||
|
||||
local Plugins = plugin.Plugins
|
||||
local API = plugin.API
|
||||
|
||||
|
||||
-- All these functions start with the 'plugin_id' argument, which
|
||||
-- will be auto-bound to the plugin name when called from a plugin.
|
||||
|
@ -34,10 +37,26 @@ function API.AddHook(plugin_id, event_name, hook_name, callback)
|
|||
end)
|
||||
end
|
||||
|
||||
function API.Sleep(plugin_id, Delay)
|
||||
reactor:Sleep(Delay)
|
||||
end
|
||||
|
||||
|
||||
function API.WaitForEvent(plugin_id, Event)
|
||||
reactor:WaitForEvent(Event)
|
||||
end
|
||||
|
||||
function API.Event(plugin_id, Event)
|
||||
reactor:Event(Event)
|
||||
end
|
||||
|
||||
|
||||
function API.AddCommand(plugin_id, command_name, arity, callback, access, help)
|
||||
bot:AddCommand(command_name, arity, function(...)
|
||||
plugin.Quota(5)
|
||||
return callback(...)
|
||||
local Result = callback(...)
|
||||
debug.sethook()
|
||||
return Result
|
||||
end, access, help)
|
||||
local PluginCommands = Plugins[plugin_id].Commands
|
||||
PluginCommands[#PluginCommands + 1] = command_name
|
||||
|
@ -63,11 +82,15 @@ end
|
|||
function plugin.Quota(seconds)
|
||||
local Start = os.time()
|
||||
debug.sethook(function()
|
||||
if debug.gethook() == nil then
|
||||
-- Race condition - somebody already removed us.
|
||||
return
|
||||
end
|
||||
if os.time() - Start > seconds then
|
||||
debug.sethook()
|
||||
error("Time quota exceeded.")
|
||||
end
|
||||
end, "", 100000)
|
||||
end, "", 1)
|
||||
end
|
||||
|
||||
function plugin.Create(plugin_id)
|
||||
|
@ -105,6 +128,29 @@ function plugin.Unload(plugin_id)
|
|||
end
|
||||
|
||||
function plugin.PrepareEnvironment(plugin_id)
|
||||
local function DeepCopy(t)
|
||||
local Copied = {}
|
||||
local Result = {}
|
||||
local function Internal(Out, In)
|
||||
for K, V in pairs(In) do
|
||||
local Type = type(V)
|
||||
if Type == "string" or Type == "function" or Type == "number" then
|
||||
Out[K] = V
|
||||
elseif Type == "table" then
|
||||
if Copied[V] ~= nil then
|
||||
Out[K] = Copied[V]
|
||||
else
|
||||
Copied[V] = {}
|
||||
Internal(Copied[V], V)
|
||||
Out[K] = Copied[V]
|
||||
end
|
||||
end
|
||||
end
|
||||
return Out
|
||||
end
|
||||
Internal(Result, t)
|
||||
return Result
|
||||
end
|
||||
local function BindPluginID(f)
|
||||
return function(...)
|
||||
local Args = {...}
|
||||
|
@ -113,15 +159,16 @@ function plugin.PrepareEnvironment(plugin_id)
|
|||
end
|
||||
|
||||
local Env = {}
|
||||
Env.table = require('table')
|
||||
Env.string = require('string')
|
||||
Env.json = require('json')
|
||||
Env.DBI = require('DBI')
|
||||
Env.table = DeepCopy(require('table'))
|
||||
Env.string = DeepCopy(require('string'))
|
||||
Env.json = DeepCopy(require('json'))
|
||||
Env.DBI = DeepCopy(require('DBI'))
|
||||
Env.print = print
|
||||
Env.error = error
|
||||
Env.tonumber = tonumber
|
||||
Env.tostring = tostring
|
||||
Env.pcall = pcall
|
||||
Env.type = type
|
||||
Env.loadstring = function(s)
|
||||
if s:byte(1) == 27 then
|
||||
return nil, "Refusing to load bytecode"
|
||||
|
@ -160,7 +207,7 @@ end
|
|||
|
||||
function plugin.AddRuntimeCommands()
|
||||
bot:AddCommand('plugin-load', 1, function(Username, Channel, Name)
|
||||
if not Name:match('([a-zA-Z0-9\-_]+)') then
|
||||
if not Name:match('([a-zA-Z0-9%-_]+)') then
|
||||
Channel:Say("Invalid plugin name!")
|
||||
return
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
local socket = require('socket')
|
||||
local coroutine = require('coroutine')
|
||||
|
||||
reactor = {}
|
||||
|
||||
|
@ -8,6 +9,10 @@ function reactor:Initialize(quantum)
|
|||
self._quantum = quantum or 0.1
|
||||
self._quit = false
|
||||
|
||||
self._co_sleep = {}
|
||||
self._co_event = {}
|
||||
self._coroutines = {}
|
||||
|
||||
self._timers = {}
|
||||
end
|
||||
|
||||
|
@ -15,6 +20,41 @@ function reactor:Quit()
|
|||
self._quit = true
|
||||
end
|
||||
|
||||
function reactor:Sleep(time)
|
||||
local co = coroutine.running()
|
||||
if co == nil then
|
||||
socket.sleep(time)
|
||||
return
|
||||
end
|
||||
self._co_sleep[co] = os.time() + time
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
function reactor:WaitForEvent(event)
|
||||
local co = coroutine.running()
|
||||
if co == nil then
|
||||
error("Main thread waiting for event... wtf?")
|
||||
end
|
||||
self._co_event[co] = event
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
|
||||
function reactor:Event(event)
|
||||
for Coroutine, Event in pairs(self._co_event) do
|
||||
if Event == event then
|
||||
coroutine.resume(Coroutine)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function reactor:Spawn(f, ...)
|
||||
local Args = {...}
|
||||
local co = coroutine.create(function() f(unpack(Args)) end)
|
||||
self._coroutines[#self._coroutines + 1] = co
|
||||
coroutine.resume(co)
|
||||
end
|
||||
|
||||
function reactor:Run()
|
||||
local read = {}
|
||||
for Socket, V in pairs(self._read_sockets) do
|
||||
|
@ -35,25 +75,35 @@ function reactor:Run()
|
|||
-- we actually got something on our sockets
|
||||
for Socket, Data in pairs(self._read_sockets) do
|
||||
if r[Socket] ~= nil then
|
||||
Socket:settimeout(2)
|
||||
local Line, Error = Socket:receive('*l')
|
||||
if Error then
|
||||
error('Could not receive line: ' .. Error)
|
||||
end
|
||||
local Callback = Data[1]
|
||||
local Args = Data[2]
|
||||
Callback(Socket, unpack(Args))
|
||||
hook.Call('SocketDataReceived', Socket)
|
||||
self:Spawn(Callback, Line, unpack(Args))
|
||||
end
|
||||
end
|
||||
for Socket, Data in pairs(self._write_sockets) do
|
||||
if w[Socket] ~= nil then
|
||||
local Callback = Data[1]
|
||||
local Args = Data[2]
|
||||
Callback(Socket, unpack(Args))
|
||||
self:Spawn(Callback, Socket, unpack(Args))
|
||||
end
|
||||
end
|
||||
end
|
||||
-- See, if we should wake up any sleepers
|
||||
for Coroutine, Timeout in pairs(self._co_sleep) do
|
||||
if os.time() > Timeout then
|
||||
self._co_sleep[Coroutine] = nil
|
||||
coroutine.resume(Coroutine)
|
||||
end
|
||||
end
|
||||
hook.Call('ReactorTick')
|
||||
local Time = os.time()
|
||||
for TimerName, Data in pairs(self._timers) do
|
||||
if Time >= Data.NextTick then
|
||||
print("Firing timer " .. TimerName)
|
||||
local Result = Data.Callback()
|
||||
if Data.Period ~= nil and Result ~= false then
|
||||
Data.NextTick = Data.NextTick + Data.Period
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
local Map = {}
|
||||
local Pending = {}
|
||||
|
||||
plugin.AddHook('auth.GetAccount', 'GetAccount', function(IRC, Username)
|
||||
if Map[Username] == nil then
|
||||
if not Pending[Username] then
|
||||
IRC:Whois(Username)
|
||||
Pending[Username] = true
|
||||
end
|
||||
plugin.WaitForEvent("whois-"..Username)
|
||||
end
|
||||
return Map[Username]
|
||||
end)
|
||||
|
||||
plugin.AddHook('irc.GetResponse330', 'WHOISResponse', function(_, Username, Account, Message)
|
||||
Map[Username] = Account
|
||||
plugin.Event("whois-" .. Username)
|
||||
end)
|
||||
|
||||
plugin.AddHook('irc.ChannelNames', 'ScanChannelUsers', function(Channel)
|
||||
for Nick, Member in pairs(Channel.Members) do
|
||||
if not Pending[Username] then
|
||||
IRC:Whois(Username)
|
||||
Pending[Username] = true
|
||||
end
|
||||
end
|
||||
end)
|
|
@ -1,18 +1,46 @@
|
|||
|
||||
local function DeepCopy(t)
|
||||
local Copied = {}
|
||||
local Result = {}
|
||||
local function Internal(Out, In)
|
||||
for K, V in pairs(In) do
|
||||
local Type = type(V)
|
||||
if Type == "string" or Type == "function" or Type == "number" then
|
||||
Out[K] = V
|
||||
elseif Type == "table" then
|
||||
if Copied[V] ~= nil then
|
||||
Out[K] = Copied[V]
|
||||
else
|
||||
Copied[V] = {}
|
||||
Internal(Copied[V], V)
|
||||
Out[K] = Copied[V]
|
||||
end
|
||||
end
|
||||
end
|
||||
return Out
|
||||
end
|
||||
Internal(Result, t)
|
||||
return Result
|
||||
end
|
||||
|
||||
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
|
||||
local Env = DeepCopy(_G)
|
||||
Env.plugin = nil
|
||||
Env.loadstring = nil
|
||||
Env.pcall = nil
|
||||
Env.setfenv = nil
|
||||
Env._G = Env
|
||||
Env.DBI = nil
|
||||
Env.print = function(...)
|
||||
local Args = {...}
|
||||
local Output = table.concat(Args, "\t")
|
||||
Channel:Say("stdout: " .. Output)
|
||||
end
|
||||
|
||||
setfenv(Function, Env)
|
||||
local Result, Message = pcall(Function)
|
||||
|
|
|
@ -29,7 +29,7 @@ local Username = config:Get('irc', 'username')
|
|||
local Realname = config:Get('irc', 'realname')
|
||||
|
||||
reactor:Initialize()
|
||||
bot:Initialize(irc, ',')
|
||||
bot:Initialize(irc, '~')
|
||||
plugin.AddRuntimeCommands()
|
||||
plugin.Discover()
|
||||
irc:Connect(Server, Port, Nickname, Username, Realname)
|
||||
|
|
Loading…
Reference in New Issue