Basic bot functionality.

master
q3k 2013-09-22 13:54:15 +02:00
parent 3de151e2bc
commit 6703df6f78
5 changed files with 359 additions and 17 deletions

57
core/bot.lua Normal file
View File

@ -0,0 +1,57 @@
-- All bot-like behaviour (response to commands, etc)
bot = {}
function bot:Initialize(IRC, Prefix)
self._irc = IRC
self._prefix = Prefix
self._commands = {}
hook.Add('irc.Message', 'bot.OnChannelMessage', function(Username, Channel, Message)
local Success, Error = pcall(function()
self:OnChannelMessage(Username, Channel, Message)
end)
if not Success then
Channel:Say("Whoops! Error when executing OnChannelMessage: " .. Error)
end
end)
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
if Command == nil then
Command = Part
else
Arguments[#Arguments + 1] = Part
end
end
if not self._commands[Command] then
Channel:Say("Unknown command '" .. Command .. "'.")
else
local CommandData = self._commands[Command]
if #Arguments ~= CommandData.Arguments then
Channel:Say(string.format("Command '%s' expects '%i' arguments, got '%i'.",
Command, CommandData.Arguments, #Arguments))
else
CommandData.Callback(Username, Channel, unpack(Arguments))
end
end
end
end
function bot:AddCommand(Name, Arguments, Callback, Help, Access)
local Command = {}
Command.Callback = Callback
Command.Access = Access or 0
Command.Help = Help or "No help available."
Command.Arguments = Arguments
self._commands[Name] = Command
end

View File

@ -16,8 +16,8 @@ function hook.Call(event_name, ...)
for K, Function in pairs(hook.Hooks[event_name]) do
if type(Function) == 'function' then
local Return = Function(unpack(Args))
if Return == false then
return
if Return ~= nil then
return Return
end
end
end

203
core/irc.lua Normal file
View File

@ -0,0 +1,203 @@
irc = {}
irc.Channel = {}
irc.Channel.__index = irc.Channel
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)
local Prefix, Command, Arguments
if Data:sub(1, 1) == ':' then
local Pattern = ':([^ ]+) +([^ ]+) *(.*)'
Prefix, Command, Arguments = string.match(Data, Pattern)
else
local Pattern = '([^ ]+) *(.*)'
Prefix = nil
Command, Arguments = string.match(Data, Pattern)
end
if Command == nil then
error('Invalid IRC line: ' .. Data)
end
self:HandleCommand(Prefix, Command, Arguments)
end
function irc:ParseArguments(Arguments)
local Parts = {}
local Current = ""
local GrabRest = false
for c in Arguments:gmatch('.') do
if GrabRest then
Current = Current .. c
elseif c == ' ' then
if Current ~= '' then
Parts[#Parts + 1] = Current
Current = ''
end
else
if Current == '' and c == ':' then
-- the rest is a single part, with spaces
GrabRest = true
else
Current = Current .. c
end
end
end
Parts[#Parts + 1] = Current
return Parts
end
function irc:HandleCommand(Prefix, Command, Arguments)
local Number = tonumber(Command)
local Arguments = self:ParseArguments(Arguments)
if Command == 'PING' then
self:_Send('PONG ' .. Arguments[1])
elseif Command == 'NOTICE' then
local Target, Message = unpack(Arguments)
hook.Call('irc.Notice', Target, Message)
elseif Command == 'MODE' then
local Target, Mode = unpack(Arguments)
hook.Call('irc.Mode', Target, Mode)
elseif Command == 'PRIVMSG' then
local Target, Message = unpack(Arguments)
local Username = Prefix:match('([^!]+)!.*')
if Target == self._nickname then
hook.Call('irc.PrivateMessage', Username, Message)
else
hook.Call('irc.Message', Username, self._channels[Target], Message)
end
elseif Number and (400 <= Number) and (Number < 500) then
if Number == 433 then
-- Nickname already in use
local New = hook.Call('irc.NicknameInUser', self._nickname)
if New and New ~= self._nickname then
self._nickname = New
else
self._nickname = self._nickname .. '_'
end
self:SetNick(self._nickname)
end
elseif Number and (Number >= 300) and (Number < 400) then
-- IRC server response. some parts of us might be looking for these
if self._response_hooks[Number] ~= nil then
for K, Callback in pairs(self._response_hooks[Number]) do
if Callback(unpack(Arguments)) ~= false then
self._response_hooks[Number][K] = nil
end
end
end
end
end
function irc:OnResponse(Response, Callback)
if type(Response) ~= 'number' then
error("Watched response is not a number.")
end
if Response >= 300 and Response < 400 then
self._response_hooks[Response] = self._response_hooks[Response] or {}
local Count = #self._response_hooks[Response]
self._response_hooks[Response][Count + 1] = Callback
else
error("Watched response is not a 3xx response.")
end
end
function irc:_Send(message)
self.Socket:send(message..'\r\n')
end
function irc:SetNick(nickname)
self:_Send('NICK '..nickname)
hook.Call('info', 'Changing nickname to ' .. self._nickname)
end
function irc:LoginUser(username, realname)
self:_Send('USER ' .. username .. ' 0 * :' .. realname)
hook.Call('info', 'Logging in as ' .. username .. ' ' .. realname)
end
function irc:Say(target, message)
self:_Send('PRIVMSG ' .. target .. ' :' .. message)
end
function irc:Join(channel)
local Channel = setmetatable({}, irc.Channel)
Channel.Name = channel
Channel.Topic = ""
Channel.Members = {}
Channel._irc = self
self._channels[channel] = Channel
self:_Send('JOIN ' .. channel)
self:OnResponse(332, function(Nick, _channel, Topic)
-- Channel topic
if _channel ~= channel then
return false
end
Channel.Topic = Topic
hook.Call('irc.ChannelTopic', Channel)
return true
end)
local MoreNicks = true
self:OnResponse(353, function(Nick, Type, _channel, Members)
if not MoreNicks then
return true
end
if _channel ~= channel then
return false
end
for Member in Members:gmatch("%S+") do
print(Member)
local Flag, Name = Member:match('([~@%&]?)(.+)')
local Data = {}
Data.Name = Name
Data.Flags = Flags
Channel.Members[Name] = Data
end
return true
end)
self:OnResponse(366, function()
MoreNicks = false
hook.Call('irc.ChannelNames', Channel)
end)
end
function irc:Connect(server, port, nickname, username, realname)
self._nickname = nickname
self._username = username
self._realname = realname
self._channels = {}
self._response_hooks = {}
-- Connection procedure (callback hell!)
local FinishedInitialNotices = function()
hook.Remove('irc.Notice', 'irc.Connect')
hook.Add('irc.Mode', 'irc.Connect', function(Target, Mode)
if Target == self._nickname then
hook.Remove('irc.Mode', 'irc.Connect')
hook.Call('irc.Connected')
end
end)
self:SetNick(self._nickname)
self:LoginUser(username, realname)
end
hook.Add('irc.Notice', 'irc.Connect', function(Target, Message)
reactor:SetTimer('irc.Connect.NoticeTimeout', 2, FinishedInitialNotices)
end)
local Socket = reactor:TCPConnect(server, port, function(socket)
irc:ReceiveData(socket)
end)
self.Socket = Socket
end

View File

@ -6,6 +6,13 @@ function reactor:Initialize(quantum)
self._read_sockets = {}
self._write_sockets = {}
self._quantum = quantum or 0.1
self._quit = false
self._timers = {}
end
function reactor:Quit()
self._quit = true
end
function reactor:Run()
@ -18,24 +25,64 @@ function reactor:Run()
write[#write+1] = Socket
end
local r, w, e = socket.select(read, write, self._quantum)
if e == nil then
-- we actually got something on our sockets
for Socket, Data in pairs(self._read_sockets) do
if r[Socket] ~= nil then
local Callback = Data[1]
local Args = Data[2]
Callback(unpack(Args))
hook.Call('SocketDataReceived', Socket)
while true do
if self._quit then
hook.Call('ReactorQuit')
break
end
local r, w, e = socket.select(read, write, self._quantum)
if e == nil then
-- we actually got something on our sockets
for Socket, Data in pairs(self._read_sockets) do
if r[Socket] ~= nil then
local Callback = Data[1]
local Args = Data[2]
Callback(Socket, unpack(Args))
hook.Call('SocketDataReceived', Socket)
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))
end
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(unpack(Args))
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
else
self._timers[TimerName] = nil
end
end
end
end
hook.Call('ReactorTick')
end
function reactor:SetTimer(name, tick_at, callback, periodic)
local Data = {}
Data.Callback = callback
if periodic then
Data.Period = tick_at
end
Data.NextTick = os.time() + tick_at
self._timers[name] = Data
end
function reactor:RemoteTimer(name)
self._timers[name] = nil
end
function reactor:TCPConnect(host, port, receive_callback, ...)
local Socket = socket.connect(host, port)
local Args = {...}
local SocketStructure = { receive_callback, Args }
self._read_sockets[Socket] = SocketStructure
return Socket
end

View File

@ -1,5 +1,40 @@
require('core.hook')
require('core.reactor')
require('core.irc')
require('core.bot')
require('socket')
local https = require('ssl.https')
require('json')
hook.Add('info', 'repl-info', function(Message)
print('INFO: ' .. Message)
end)
hook.Add('debug', 'repl-debug', function(Message)
print('DEBUG: ' .. Message)
end)
hook.Add('irc.Connected', 'repl-connected', function()
irc:Join('#hackerspace-pl-bottest')
end)
reactor:Initialize()
bot:Initialize(irc, ',')
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')
reactor:Run()