IRC Bot com LuaSocket

LUA
Enviado por Eclesiastes em Seg, 12/02/2007 - 14:00.LUA

Olá pessoal, minha primeira e por sinal simples implementação em Lua foi a de um IRC Bot capaz de responder a um comando dado pelo owner (proprietário) e responder a mensagens de usuários do canal. Segue abaixo uma breve explicação dos passos básicos para criação de um bot.

Como diz no título, eu utilizei a extensão LuaSocket, que provê suporte para TCP e UDP, e um conjunto de módulos com funcionalidades comuns de uma aplicação para internet. (HTTP, FTP, MIME, etc)

O site da extensão é http://www.cs.princeton.edu/~diego/professional/luasocket/, e tem um link explicando sobre a instalação, é bem simples!

Vamos aos códigos!

  1. irc = require("socket")

Assim temos na variável global irc acesso as funções e variáveis globais do módulo requerido.

Para fazer a conexão é bem simples, basta informar o server e a porta, por exemplo:

  1. -- Conexão
  2. client = irc.connect('irc.brasnet.org', 6667)

Para testar se a conexão foi estabelecida:

  1. if client == nil then
  2.     print('Nao foi possivel conectar!')
  3.     os.exit()
  4. end

Para especificarmos que nossa conexão é Keep-alive:

  1. client:setoption('keepalive', true)

Bom, já com a conexão estabelecida, precisamos enviar umas informações do cliente (do bot) para o servidor de IRC.

  1. -- Dados do Bot
  2. bot = {}
  3.  
  4. -- Nick do bot
  5. bot.nick = 'myLUABot'
  6.  
  7. -- Nome do bot
  8. bot.name = 'LuaBot - by Eclesiastes'
  9.  
  10. -- Identd
  11. bot.identd = 'bot.lua'
  12.  
  13. -- Canal a entrar
  14. bot.channel = '#a,#b,#c'
  15.  
  16. client:send("USER " .. bot.identd .. " 2 3 :" .. bot.name .. "\r\n")
  17. client:send("NICK " .. bot.nick .. " " ..  bot.identd .. "\r\n")
  18. client:send("JOIN " .. bot.channel .. "\r\n")
  19. client:send("PRIVMSG " .. bot.channel .. "\r\n")

Assim feito, o bot estará identificado no servidor.

Pronto, a partir daí basta fazer um loop que termina somente quando não receber mais dados do servidor. E de acordo com os dados, você pode programar o bot para efetuar uma operação correspondente. Como por exemplo, responder a mensagem PING, caso não seja respondida, acarretará em desconexão.
Deixo então essa operação para vocês! Deixo abaixo o meu código-fonte para solucionar alguma possível dúvida.
E qualquer dúvida quanto ao protocolo, acesse: http://www.irchelp.org/irchelp/rfc/.

> bot.lua

  1. ------------------------------------------------------------------------------
  2. -- LuaBot - IRC bot
  3. -- Author: Felipe Nascimento S. Pena
  4. -- E-mail: felipensp [at] gmail [dot] com
  5. -- Date  : 2007-02-07
  6. ------------------------------------------------------------------------------
  7.  
  8. irc = require("socket")
  9. require("ecl.bot.botlib")
  10.  
  11. -- Verificando os argumentos
  12.  
  13. -- IP do servidor
  14. if arg[1] == nil then
  15.     print('Informe o server!')
  16.     os.exit()
  17. end
  18.  
  19. local server = arg[1]
  20.  
  21. -- Porta
  22. if arg[2] == nil then
  23.     print('Informe a porta!')
  24.     os.exit()
  25. end
  26.  
  27. local port = arg[2]
  28.  
  29. -- Canal
  30. if arg[3] == nil then
  31.     print('Informe o canal!')
  32.     os.exit()
  33. end
  34.  
  35. -- Dados do Bot
  36.  
  37. bot = {}
  38.  
  39. -- Nick do bot
  40. bot.nick = 'LUABot'
  41.  
  42. -- Nome do bot
  43. bot.name = 'LuaBot - by Eclesiastes'
  44.  
  45. bot.identd = 'bot.lua'
  46.  
  47. -- Dono do bot
  48. bot.owner = 'ecl'
  49.  
  50. -- Canal a entrar
  51. bot.channel = arg[3]
  52.  
  53.  
  54. -- Conexão
  55. client = irc.connect(server, port)
  56.  
  57. if client == nil then
  58.     print('Nao foi possivel conectar!')
  59.     os.exit()
  60. end
  61.  
  62. client:setoption('keepalive', true)
  63.  
  64. print('>> LUA Bot (by Eclesiastes)')
  65. print('Conectando em ' .. client:getpeername())
  66. print('---------------------------------------')
  67.  
  68. client:send("USER " .. bot.identd .. " 2 3 :" .. bot.name .. "\r\n")
  69. client:send("NICK " .. bot.nick .. " " ..  bot.identd .. "\r\n")
  70. client:send("JOIN " .. bot.channel .. "\r\n")
  71. client:send("PRIVMSG " .. bot.channel .. "\r\n")
  72.  
  73. -- Definindo o cliente e os dados do bot
  74. luabot:set(client, bot)
  75.  
  76. -- Mensagem de entrada no canal
  77. luabot:sendtochannel('y0! Ladies & Gentlemen! :Þ')
  78.  
  79. repeat
  80.  
  81.     -- Recebendo mensagem
  82.     msg = client:receive() 
  83.    
  84.     -- Exibe as mensagens recebidas
  85.     -- print(msg)
  86.    
  87.     -- Verifica mensagem de ping   
  88.     luabot:ping(msg)
  89.    
  90.     -- Verifica mensagens recebidas e responde se necessário
  91.     luabot:receive(msg)       
  92.        
  93. until msg == nil
  94.  
  95. -- Stats
  96. -- Bytes enviados, recebidos em segundos
  97. print('Stats')
  98. print('---------------------------------------')
  99. print('Enviado\tRecebido')
  100. print(client:getstats())

O caminho está "ecl.bot.botlib" pois a botlib.lua está em C:\lua\ecl\bot\.

> botlib.lua

  1. ------------------------------------------------------------------------------
  2. -- Funcionalidades do bot
  3. -- LuaBot - IRC bot
  4. -- Author: Felipe Nascimento S. Pena
  5. -- E-mail: felipensp [at] gmail [dot] com
  6. -- Date  : 2007-02-07
  7. ------------------------------------------------------------------------------
  8.  
  9. luabot = {}
  10.  
  11. -- Define a conexão e os dados do bot
  12. function luabot:set(client, bot)
  13.     self.client = client
  14.     self.bot = bot
  15. end
  16.  
  17. -- Ping pong
  18. function luabot:ping(msg) 
  19.     if msg ~= nil then
  20.         local _,_, daemon = string.find(msg, '^PING :(.+)')
  21.  
  22.         if daemon ~= nil then
  23.             self.client:send('PONG :' .. daemon)
  24.         end
  25.     end
  26. end
  27.  
  28. -- Envia mensagem para um canal
  29. function luabot:sendtochannel(msg, chan)
  30.     if chan == nil then
  31.         self.client:send('PRIVMSG ' .. self.bot.channel .. ' :' .. msg .. "\r\n")
  32.     else
  33.         self.client:send('PRIVMSG ' .. chan .. ' :' .. msg .. "\r\n")
  34.     end
  35. end
  36.  
  37. -- Envia mensagem para um usuário
  38. function luabot:sendtonick(chan, nick, msg)
  39.     self.client:send('PRIVMSG ' .. chan .. ' :' .. nick .. ': ' .. msg .. "\r\n")
  40. end
  41.  
  42. -- Entra em um canal
  43. function luabot:join(channel)
  44.     self.client:send('JOIN ' .. channel .. '\r\n')
  45. end
  46.  
  47. -- Sai de um canal
  48. function luabot:part(channel)
  49.     self.client:send('PART ' .. channel .. '\r\n')
  50. end
  51.  
  52. -- Executa uma ação enviada solicitada pelo owner
  53. function luabot:owner_cmd(chan, cmd)
  54.     -- Debug
  55.     -- print('O chefe mandou: ' .. cmd)
  56.            
  57.     -- Fechar conexão
  58.     if string.find(cmd, '^exit$') then   
  59.        
  60.         self:quit()   
  61.        
  62.     -- Sair de um canal
  63.     elseif string.find(cmd, '^part') then
  64.    
  65.         local _,_, chan = string.find(cmd, '^part (#%S+)')
  66.        
  67.         if chan ~= nil then
  68.             self:part(chan)
  69.         end 
  70.          
  71.     -- Entrar em canal       
  72.     elseif string.find(cmd, '^join') then
  73.        
  74.         local _,_, chan = string.find(cmd, '^join (#%S+)')
  75.                  
  76.         if chan ~= nil then
  77.             self:join(chan)   
  78.         end
  79.        
  80.     elseif string.find(cmd, '^kiss') then
  81.        
  82.         local _,_, user = string.find(cmd, '^kiss (%S+)')
  83.        
  84.         if user ~= nil then
  85.             self:sendtonick(chan, user, ':@;')
  86.         end
  87.                    
  88.     end
  89. end
  90.  
  91. -- Responde uma mensagem enviada ao bot
  92. function luabot:user_msg(chan, sender, msgr)
  93.     -- Debug
  94.     -- print('O user ' .. sender .. ' disse para mim: ' .. msgr)
  95.    
  96.     -- Retribuindo um beijo
  97.     if string.find(msgr, '^[:;]@[:;]') then
  98.  
  99.         self:sendtonick(chan, sender, msgr)
  100.  
  101.     -- Respondendo uma pergunta
  102.     elseif string.find(msgr, '^qual eh o seu nome?') then
  103.  
  104.         self:sendtonick(chan, sender, 'Nem sei hein... :Þ')
  105.  
  106.     -- Respondendo um olhar espantado
  107.     elseif string.find(msgr, '^o[.,_]o') then
  108.  
  109.         self:sendtonick(chan, sender, 'O.o')
  110.  
  111.     end   
  112. end
  113.  
  114. -- Lê os dados recebidos e responde se necessário
  115. function luabot:receive(msg)
  116.     if msg ~= nil then   
  117.      
  118.         -- Comando do owner no canal
  119.         local _,_, chan, cmd = string.find(msg, ':' .. self.bot.owner .. '!%S+ PRIVMSG (#%S+) :!(.+)')
  120.        
  121.         if cmd ~= nil then         
  122.             self:owner_cmd(chan, string.lower(cmd))         
  123.             return
  124.         end
  125.                  
  126.         -- Mensagens no canal para o bot
  127.         local _,_, sender, chan, msgr = string.find(msg, ':([^!]+)!%S+ PRIVMSG (%S+) :' .. self.bot.nick ..': (.+)')
  128.              
  129.         if msgr ~= nil then         
  130.             self:user_msg(chan, sender, string.lower(msgr))           
  131.             return
  132.         end   
  133.        
  134.         -- Mensagens no canal
  135.         -- local _,_, sender, chan, msgr = string.find(msg, ':([^!]+)!%S+ PRIVMSG (%S+) :!(.+)')           
  136.     end
  137. end
  138.  
  139. -- Fechar conexão
  140. function luabot:quit()
  141.     self.client:send("QUIT :É duro ser bot! :~\r\n")
  142.     self.client:close()
  143. end

Como podem ver, eu passo o server, porta e canal como argumento.
Por exemplo:

c:\lua>lua "ecl/bot/bot.lua" irc.brasnet.org 6665 #phpavancado

Sobre as funções disponíveis para um conexão TCP usando LuaSocket:
http://www.cs.princeton.edu/~diego/professional/luasocket/tcp.html

Até a próxima!



Enviado por tomas (não verificado(a)) em Seg, 13/08/2007 - 23:58.

Desculpem, mas não consegui fazer a indentação corretamente :-(

Você pode usar a função assert para economizar um pouco o trabalho. Por exemplo, trocar:

if client == nil then
print('Nao foi possivel conectar!')
os.exit()
end

por:

assert (client, 'Não foi possível conectar!')

Ou ainda:

local server = assert (arg[1], 'Informe o server!')

Outra boa prática é dar ao módulo o mesmo nome do arquivo, pois isso facilita a leitura do código. Por exemplo, ao invés de:

require("ecl.bot.botlib")

use:

local luabot = require("ec1.bot.botlib")

para deixar claro qual variável está guardando o módulo. O ideal mesmo seria fazer isto tudo dentro do módulo, ou seja, renomear o arquivo para ec1/bot/luabot.lua e usar a função module para definir o módulo:

module(...) -- assim ele vai ter o mesmo nome do arquivo :-)
function set (self, client, bot)
self.client = client
...

Tem o chato de você ter que escrever self, explicitamente, mas você economiza no nome do módulo :-)

Em alguns casos a string.match é mais útil que a find. Por exemplo no trecho:

local _,_, daemon = string.find(msg, '^PING :(.+)')

você pode escrever simplesmente:

local daemon = string.match(msg, '^PING :(.+)')

ou ainda:

local daemon = msg:match('^PING :(.+)')

Você também pode simplificar aqui:

function luabot:sendtochannel(msg, chan)
if chan == nil then
self.client:send('PRIVMSG ' .. self.bot.channel .. ' :' .. msg .. "\r\n")
else
self.client:send('PRIVMSG ' .. chan .. ' :' .. msg .. "\r\n")
end
end

para:

function sendtochannel(self, msg, chan)
chan = chan or self.bot.channel
self.client:send('PRIVMSG ' .. chan .. ' :' .. msg .. "\r\n")
end

Aquela seqüencia:

if string.find(cmd, '^exit$') then
...
elseif string.find(cmd, '^part') then
...
elseif string.find(cmd, '^join') then
...
elseif string.find(cmd, '^kiss') then
...
end

poderia ficar bem mais legível:

local acoes = {
exit = function (self) self:quit() end,
part = function (self, chan)
chan = chan:match('^ (#%S+)')
if chan then self:part(chan) end
end,
join = function (self, chan) ... end,
kiss = function (self, chan) ... end,
}
local acao = acoes[cmd:match('^(....)')]
if acao then
acao (self, chan)
end

Eu não testei nada disso, mas espero que tenha dado para entender :-)