378 lines
12 KiB
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
|
|
|