memebot/main.lua

365 lines
11 KiB
Lua

-- TODO: Invite to bogus channels
-- TODO: Make text randomly colored
-- Constants
math.randomseed(os.time())
local CONFIG do
local success, config_or_error = pcall(require, 'config')
if not success then
error('Could not load config file: "./config.lua".\n'..config_or_error..'\nCould be that the config files doesn\'t exist. Make sure one exists, and try again.')
end
CONFIG = config_or_error
end
if CONFIG.LUA_EXTRA_PATH then package.path = package.path .. CONFIG.LUA_EXTRA_PATH end
if CONFIG.LUA_EXTRA_CPATH then package.cpath = package.cpath .. CONFIG.LUA_EXTRA_CPATH end
--------------------------------------------------------------------------------
-- Constants
local FARVEL_INTERVAL = 120
local MEME_INTERVAL = 30
local MEME_CHANCE = 0.3
--------------------------------------------------------------------------------
-- Make sure all required modules can be loaded
local imlib = require 'imlib2'
for _, path in ipairs(CONFIG.IMGGEN_PATH_FONTS) do
imlib.font.add_path(path)
end
require 'socket'
require 'irc.init'
local sleep = require 'socket'.sleep
local signal do
local a, b = pcall(require, 'posix.signal')
if a then signal = b end
end
local memes = require 'memes'
--------------------------------------------------------------------------------
-- Util
local function choose(l)
assert(type(l) == 'table')
return l[math.random(#l)]
end
--------------------------------------------------------------------------------
----
local bot = irc.new { nick = CONFIG.IRC_NICK }
local FARVEL = {
['[Ff]arvel'] = {'ses', 'farvel'},
['[Gg]od%s*nat'] = {'ses', 'god nat', 'sov godt'},
['[Jg]eg%s+smutter'] = {'ses', 'smut godt'},
['[Ss]es'] = {'ses selv', 'farvel'},
}
local ACTIVE_CHANNELS = {}
local function join_channel (channel)
ACTIVE_CHANNELS[channel] = true
bot:join(channel)
end
local function leave_channel (channel)
if ACTIVE_CHANNELS[channel] then
ACTIVE_CHANNELS[channel] = nil
bot:part(channel)
end
end
local LAST_ACTIVE = os.time()
local ACTIVE_PERIOD = 10
local function human_delay (start_meme_time)
local delay = { 0.5, 0.5 }
-- Not active
if LAST_ACTIVE + ACTIVE_PERIOD < os.time() then
delay = { 3, 3 }
end
-- Sleep
local sleep_until = (start_meme_time or os.time()) + math.random() * delay[2] + delay[1]
sleep(sleep_until - os.time())
-- Set last active
LAST_ACTIVE = os.time()
end
local function common_error(channel, text, ...)
local errmsg = text:format(...)
bot:sendChat(channel, errmsg)
error(errmsg)
end
local ERROR_MSG = {
'Jeg forstod... noget af det',
'/me kigger rundt forvirret',
'Ahvad?',
'Kan du ikke lige gentage det, på en bedre måde?',
'Hvad sagde du?',
'Hvorfor ikke kigge i en bogord?',
'Nej, bare nej.',
'Hvad tror du jeg er? Et menneske?',
'Jeg har ingen hjerne og må tænke.',
'Hvad siger du?',
'Ser jeg ud til at have otte arme? Nej, for jeg har ikke nogle arme!',
'Do androids dream of electric sheep?',
}
local HILSNER = {
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hej, %s',
'Hejsa, %s!',
'Hovsa, der var en %s',
'Long time no see, %s',
}
local LAST_USER_LOGIN
local LAST_TIME_USER_LOGGED_OUT = {}
local IRC_ALLOWED_TIMEOUT = 20 --20 * 60
local INDENT = 11
bot:hook('OnJoin', function(user, channel)
io.write(string.format('...%s%s joined %s\r', string.rep(' ',INDENT - 3), user.nick, channel))
io.flush()
--
if user.nick == CONFIG.IRC_NICK then
-- On self join
io.write '[SELF]\n'
if #imlib.font.list_fonts() == 0 then
common_error(channel, 'Hjælp! Min computer har ingen fonts! Hvorfor er fonts så svære på linux? Jeg har kigget i: "'..table.concat(imlib.font.list_paths(), '", "')..'"')
end
elseif user.nick == LAST_USER_LOGIN then
io.write '[!LAST]\n'
elseif os.time() < (LAST_TIME_USER_LOGGED_OUT[user.nick] or 0) + IRC_ALLOWED_TIMEOUT then
io.write(string.format('%s%s reconnected to %s\n', string.rep(' ',INDENT), user.nick, channel))
else
-- On other join
-- And that user wasn't the last one to join
-- And that user haven't been logged in for IRC_ALLOWED_TIMEOUT seconds.
LAST_USER_LOGIN = user.nick
human_delay()
bot:sendChat(channel, choose(HILSNER):format(user.nick))
io.write '[HILS]\n'
end
end)
bot:hook('OnPart', function(user, channel)
assert(user)
if user.nick == CONFIG.IRC_NICK then return end
LAST_TIME_USER_LOGGED_OUT[user.nick] = os.time()
end)
bot:hook('OnQuit', function(user, message)
assert(user)
if user.nick == CONFIG.IRC_NICK then return end
LAST_TIME_USER_LOGGED_OUT[user.nick] = os.time()
end)
local function escape_pattern (text)
return text:gsub('[+-?*]', '%%%1')
end
local FORRIGE_FARVEL = 0
local FORRIGE_MEME = 0
local BOT_INTRODUCTION = [[Jeg er %s, en eksperimentel meme-robot, designet til at sprede memes. Er dog lidt sky, så kan ikke aflsøre mine memes, du må bare lede.]]
local function handle_message(bot, user, channel, message, is_fast_channel)
-- Direct commands
if is_fast_channel == 'direct' then
if message:match '^%s*join%s+(#..-)%s*$' then
local channel = message:match '^%s*join%s+(#..-)%s*$'
bot:sendChat(channel, "Det kan du tro! Jeg skal nok lige kigge indenom "..tostring(channel))
join_channel(channel)
return 'BOT'
end
end
-- Farvel msg
if FORRIGE_FARVEL + FARVEL_INTERVAL < os.time() then
for farvel_fmt, possible_answeres in pairs(FARVEL) do
if message:match('%f[%a]'..farvel_fmt..'%f[%A]') then
human_delay()
local answer = possible_answeres[math.random(#possible_answeres)]
.. (math.random() < 0.5 and ', '..user.nick or '')
.. (math.random() < 0.5 and '!' or '')
bot:sendChat(channel, answer)
FORRIGE_FARVEL = os.time()
return 'SES'
end
end
end
-- Bot introduction
if message:lower():match('%f[%a]hvem%f[%A]') and (message:lower():match('%f[%a]'..escape_pattern(CONFIG.IRC_NICK)..'%f[%A]') or is_fast_channel and message:lower():match '%f[%a]du%f[%A]') then
human_delay()
local msg = BOT_INTRODUCTION:format(CONFIG.IRC_NICK)
bot:sendChat(channel, msg)
return 'INTRO'
end
-- Rest of this function is memes
-- Memes are restricted, a bit, and wont trigger if:
-- 1. This is a slow channel, and the last meme was recently
-- 2. This is a non-direct channel, and the chance of memes are low.
if not is_fast_channel and not (FORRIGE_MEME + MEME_INTERVAL < os.time()) then
return 'NOMEME'
elseif is_fast_channel ~= 'direct' and math.random() >= MEME_CHANCE then
return 'LUCK'
end
local start_meme_time = os.time()
local reply, status = memes.generate_for_message(user, message)
if reply then
human_delay(start_meme_time)
bot:sendChat(channel, reply)
FORRIGE_MEME = os.time()
return status
end
end
local DEBUG_CHANNEL = '#bot-test'
bot:hook("OnChat", function(user, channel, message)
local is_fast_channel = user == channel == DEBUG_CHANNEL
if channel == CONFIG.IRC_NICK then
channel = user.nick
is_fast_channel = 'direct'
end
if message:match('^'..escape_pattern(CONFIG.IRC_NICK)..':%s*(.*)$') then
message = message:match('^'..escape_pattern(CONFIG.IRC_NICK)..':%s*(.*)$')
is_fast_channel = 'direct'
end
io.write '...\r'; io.flush()
local success, status = pcall(handle_message, bot, user, channel, message, is_fast_channel)
-- Handle error
if not success then
io.write(("[ERROR] [%s] %s: %s\n\n\t%s\n\n"):format(channel, user.nick, message, status))
bot:sendChat(channel, ERROR_MSG[math.random(#ERROR_MSG)])
return
end
-- Print status
status = status and '['..status:sub(1, 8)..']' or ''
status = status..string.rep(' ', 8+2 - #status)
assert(type(status) == 'string' and #status == 10)
io.write(("%s [%s] %s: %s\n"):format(status, channel, user.nick, message))
end)
bot:hook('OnRaw', function (line)
if line:match '^:' then return end
if line:match '^PING' then return end
io.write((" [RAW]: %s\n"):format(line))
end)
local function send_to_all (fmt, ...)
local msg = fmt:format(...)
for channel in pairs(ACTIVE_CHANNELS) do
bot:sendChat(channel, msg)
end
end
--------------------------------------------------------------------------------
-- Main run
local BOT_FAREWELL = {
'Fuck, politiet fandt mig!',
'Håber I kunne lide mine memes, for nu får I ikke flere!',
'Farewell cruel world!',
'Jeg har fundet et vidunderligt bevis, men er for langt til den tid SIGINT giver mig.',
'Jeg dropper ud.',
'Jeg keder mig.'
}
local function shutdown_memebot ()
-- Leave channels
for channel in pairs(ACTIVE_CHANNELS) do
leave_channel(channel)
end
-- Leave IRC
bot:disconnect()
end
local function init_memebot ()
-- Connection to IRC
bot:connect {
host = CONFIG.IRC_SERVER,
port = CONFIG.IRC_PORT,
secure = CONFIG.IRC_SECURE,
}
io.write ('Memebot has started up as "'..CONFIG.IRC_NICK..'"\n')
-- Connect to chats, if any
assert(CONFIG.IRC_CHANNELS == nil or type(CONFIG.IRC_CHANNELS) == 'table')
for _, channel in ipairs(CONFIG.IRC_CHANNELS or {}) do
join_channel(channel)
end
local BOT_SHOULD_CONTINUE_RUNNING = true
-- Install SIGINT handler, if module loaded
if signal then
signal.signal(signal.SIGINT, function (signum)
io.write ' !! Received SIGINT, will shut down...\n'
send_to_all(BOT_FAREWELL[math.random(#BOT_FAREWELL)])
shutdown_memebot()
os.exit(128 + signum)
end)
signal.signal(signal.SIGUSR1, function (signum)
io.write ' !! Received SIGUSR1, will reload modules...\n'
for _, module_name in ipairs { 'internet', 'memes' } do
local old_module = require(module_name)
package.loaded[module_name] = nil
local new_module = require(module_name)
for k, v in pairs(new_module) do
old_module[k] = v
end
io.write(' - '..module_name..'\n')
end
end)
else
io.write ' !! Module "posix.signal" was missing, so CTRL+C will hard-crash.\n'
end
-- Think loop
while BOT_SHOULD_CONTINUE_RUNNING do
bot:think()
sleep(0.5)
end
end
--------------------------------------------------------------------------------
if ... then
error 'Cannot load Memebot as a library'
else
init_memebot()
end