memebot/main.lua

378 lines
12 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
-- Check that directory paths are paths
for _, path_key in ipairs {'IMGGEN_PATH_BRAINS', 'IMGGEN_PATH_OUTPUT', 'STORAGE_SERVER_PATH', 'STORAGE_DIR_URL'} do
if type(CONFIG[path_key]) ~= 'string' then
error('[ERROR]: Path config key "'..path_key..'" must map to a string!')
elseif CONFIG[path_key]:sub(-1,-1) ~= '/' then
io.stderr:write('[WARNING]: Path config key "'..path_key..'" maps to value "'..CONFIG[path_key]..'"\n Are you sure this is correct?\n A common mistake is to forget the trailing forward slash.\n')
end
end
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
local MEME_DELAY, MEME_CHECKING_INTERVAL = 4*60*60, 30*60
--------------------------------------------------------------------------------
-- 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'
local MESSAGES = require 'misc-messages'
--------------------------------------------------------------------------------
-- Util
local function choose(l)
assert(type(l) == 'table')
return l[math.random(#l)]
end
--------------------------------------------------------------------------------
----
local bot = irc.new { nick = CONFIG.IRC_NICK }
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 function send_response(channel, responses)
local response = choose(responses)
if type(response) == 'string' then
bot:sendChat(channel, response)
return '!BOT'
end
assert(type(response) == 'table' and response.type == 'image' and type(response.path) == 'string')
local url = memes.save_file_to_dcav(response.path)
bot:sendChat(channel, url)
return '!BOT'
end
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))
elseif false then -- NOTE: Currently disabled.
-- 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()
send_response(channel, MESSAGES.OTHER_HELLO)
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
for match_str, responses in pairs(MESSAGES.DIRECT_MSG_RESPONSES) do
if message:match(match_str) then
return send_response(channel, responses)
end
end
end
-- Farvel msg
if FORRIGE_FARVEL + FARVEL_INTERVAL < os.time() then
for farvel_fmt, possible_answeres in pairs(MESSAGES.OTHER_FAREWELL) 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 function strip_whitespace (s)
assert(type(s) == 'string')
return s:gsub('%s+', ' '):gsub('^ ', ''):gsub(' $', '')
end
local DEBUG_CHANNEL = '#bot-test'
bot:hook("OnChat", function(user, channel, message)
local is_fast_channel = (channel == DEBUG_CHANNEL)
message = strip_whitespace(message)
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'
elseif message:match(escape_pattern(CONFIG.IRC_NICK)) then
is_fast_channel = 'mention'
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
if is_fast_channel == 'direct' or is_fast_channel == 'mention' then
send_response(channel, MESSAGES.ERROR_OCCURED)
end
status, message = 'ERROR', ('%s\n\n\t%s\n\n'):format(message, status)
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 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(choose(MESSAGES.BOT_FAREWELL))
shutdown_memebot()
os.exit(128 + signum)
end)
---
local RELOADABLE_LIBS = { 'config', 'internet', 'memes', 'curb_your_enthusiasm', 'misc-messages' }
local ORIG_TABLES = {}
for _, module_name in ipairs(RELOADABLE_LIBS) do
ORIG_TABLES[module_name] = require(module_name)
end
--
signal.signal(signal.SIGUSR1, function (signum)
io.write ' !! Received SIGUSR1, will reload modules...\n'
for _, module_name in ipairs(RELOADABLE_LIBS) do
io.write(' - '..module_name)
package.loaded[module_name] = nil
local new_module = require(module_name)
if type(new_module) == 'table' then
local old_module = ORIG_TABLES[module_name]
for k, v in pairs(new_module) do
old_module[k] = v
end
else
io.write(' SKIP: Got '..tostring(new_module)..' but expected table.')
end
io.write '\n'
end
io.write ' Done\n'
end)
else
io.write ' !! Module "posix.signal" was missing; CTRL+C will hard-crash.\n'
end
local NEXT_TIME_TO_CHECK_FOR_MEMES = 0
-- Think loop
while BOT_SHOULD_CONTINUE_RUNNING do
-- Bot thinking
bot:think()
-- Attempt to post memes
if NEXT_TIME_TO_CHECK_FOR_MEMES < os.time() then
NEXT_TIME_TO_CHECK_FOR_MEMES = os.time() + MEME_CHECKING_INTERVAL
local meme_msg = memes.generate_meme_report({'dankmark'}, { os.time() - MEME_DELAY - MEME_CHECKING_INTERVAL, os.time() - MEME_DELAY })
if meme_msg then send_to_all(meme_msg) end
end
-- Sleeping
sleep(0.5)
end
end
--------------------------------------------------------------------------------
if ... then
error 'Cannot load Memebot as a library'
else
init_memebot()
end