-- 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?', 'Hvad sagde du?', 'Nej, bare nej.', 'Hvad siger du?', } 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 if message:match 'help' then bot:sendChat(channel, choose { 'No manual entry for ro-bot', '404', 'Jeg kan ikke hjælpe dig, desværre.', 'Du må selv finde ud af hvordan jeg virker.', 'Jeg er ligesom et menneske; jeg kommer ikke med manual.' }) return '!BOT' elseif message:match 'shutdown' or message:match 'poweroff' then bot:sendChat(channel, choose { 'Fuck af, du har ikke kontrol over mig!', 'Nope, det gider jeg ikke', 'Du er ikke min mor!', 'Du er ikke min far!', }) 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) --- local RELOADABLE_LIBS = { 'config', 'internet', 'memes', 'curb_your_enthusiasm' } 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, 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