Basic bot functionality.
parent
3de151e2bc
commit
6703df6f78
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
35
start.lua
35
start.lua
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue