-- 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' require 'socket' local json = require 'json' 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 internet = require 'internet' -------------------------------------------------------------------------------- -- Meme utils for _, path in ipairs(CONFIG.IMGGEN_PATH_FONTS) do imlib.font.add_path(path) end local function flatten_onto(target_img, other_img, x0, y0) assert(x0 + other_img:get_width() <= target_img:get_width()) assert(y0 + other_img:get_height() <= target_img:get_height()) for x = 0, other_img:get_width() do for y = 0, other_img:get_height() do target_img:draw_pixel(x + x0, y + y0, other_img:get_pixel(x, y)) end end -- return end local function clean_text (text) return text:gsub('%s+', ' '):match('^%s*(.-)%s*$') end -------------------------------------------------------------------------------- -- Meme creations local function generate_bait_link() return 'https://dcav.pw/jbait' end local function fit_font_to_line_height (font_name, height) assert(type(font_name) == 'string') assert(type(height) == 'number') -- local size_min, size_max = 1, 50 while true do local size_guess = math.floor((size_max + size_min)/2) assert(size_min <= size_guess and size_guess <= size_max) local font = assert(imlib.font.load(font_name..'/'..size_guess)) if size_guess == size_min then return font, size_guess end local text_w, text_h = font:get_size 'k' -- if text_h <= height then size_min = size_guess else size_max = size_guess end end return nil end local function load_font_of_size (font_name, size) return assert(imlib.font.load(font_name..'/'..size)) end local function determine_required_font_size (font_name, text, width) assert(type(font_name) == 'string') assert(type(text) == 'string') assert(type(width) == 'number') -- local size_min, size_max = 1, 50 while true do local size_guess = math.floor((size_max + size_min)/2) assert(size_min <= size_guess and size_guess <= size_max) local font = load_font_of_size(font_name, size_guess) if size_guess == size_min then return font, size_guess end local text_w, text_h = font:get_size(text) -- if text_w <= width then size_min = size_guess else size_max = size_guess end end return nil end local function wrap_lines (font, text, width) local lines, current, x = {}, {}, 0 local SPACE_ADVANCE = font:get_advance ' ' for word in text:gmatch '[^%s]+' do assert(type(word) == 'string') local word_w = font:get_advance(word) local next_x = x + word_w + SPACE_ADVANCE if word == '' then -- Do nothing elseif next_x <= width then current[#current+1], x = word, next_x else lines[#lines+1], current = table.concat(current, ' '), { word } x = word_w end end -- Færdiggør sidste linje lines[#lines+1] = table.concat(current, ' ') -- Ret return lines end local COLOR_WHITE = imlib.color.new(255, 255, 255) local COLOR_BLACK = imlib.color.new( 0, 0, 0) local function draw_centered_lines (draw_onto, lines, x0, y0, width, font, font_color) assert(type(lines) == 'table') local y = y0 for i, line in ipairs(lines) do local text_w, text_h = font:get_size(line) local x = x0 + (width - text_w) / 2 draw_onto:draw_text(font, line, x, y, font_color or COLOR_BLACK) -- y = y + text_h end end local function determine_font_and_lines_for_box (font_name, text, width, height) local num_lines, last_actual, actual_lines = 1, nil, nil for iteration = 1, 100 do local estimate_font = determine_required_font_size(font_name, text, width * num_lines) local line_height = select(2, estimate_font:get_size(text)) local estimate_num_lines = height / select(2, estimate_font:get_size(text)) local lines = wrap_lines(estimate_font, text, width) local actual_num_lines = #lines if #lines * line_height >= height then estimate_num_lines = estimate_num_lines - 1 end -- Test for convergence if last_actual == actual_num_lines then actual_lines = lines break end num_lines = (estimate_num_lines + actual_num_lines) / 2 last_actual = actual_num_lines end assert(actual_lines) -- Find final font size local _, min_font_size = fit_font_to_line_height(font_name, height / #actual_lines) for _, line in ipairs(actual_lines) do min_font_size = math.min(min_font_size, select(2, determine_required_font_size(font_name, line, width))) end local actual_font = load_font_of_size(font_name, min_font_size) -- Assertions and error correction if (#actual_lines * select(2, actual_font:get_size(text)) < height) then io.write(' !! Performed bad fitting of text: '..text..'\n') actual_font, actual_lines = determine_required_font_size(font_name, text, width), {text} end return actual_font, actual_lines end local function draw_centered_text_in_box (font_name, draw_onto, text, x0, y0, width, height, bg_color, font_color) assert(type(font_name) == 'string') local font, lines = determine_font_and_lines_for_box(font_name, text, width, height) -- local y = y0 + (height - #lines * select(2, font:get_size(text))) / 2 -- if bg_color then draw_onto:fill_rectangle(x0, y0, width, height, bg_color) end draw_centered_lines(draw_onto, lines, x0, y, width, font, font_color) end local function choose_random_font () local fonts = imlib.font.list_fonts() assert(#fonts > 0, 'Could not find any fonts') return fonts[math.random(#fonts)] end local function load_random_font () local font_name = choose_random_font() local font, errmsg = imlib.font.load(font_name..'/30') assert(font, errmsg) return font_name end local DROSTE_EFFECT_TRIGGERS = { ['induktion'] = true, ['rekursion'] = true, ['uendelig rekursion'] = true, ['rekursive'] = true, ['rekursive funktioner'] = true, ['recursion'] = true, ['infinite recursion'] = true, ['tail recursion'] = true, ['recursive'] = true, ['recursive function'] = true, ['induktion'] = true, ['droste'] = true, ['drostle'] = true, -- Misspelling ['uendelig'] = true, ['uendeligt'] = true, ['strange loop'] = true, ['loop'] = true, } local DROSTE_ITERATIONS_DEFAULT = 5 local function fill_in_topics_information (topics) assert(type(topics) == 'table') -- local topic_to_image_url = internet.search_images(topics) -- local new_topics = {} for i, topic in ipairs(topics) do assert(type(topic) == 'string') local url = topic_to_image_url[topic] if DROSTE_EFFECT_TRIGGERS[topic] then new_topics[i] = { topic = topic, type = 'droste' } elseif url then new_topics[i] = { topic = topic, type = 'image', url = url } else new_topics[i] = { topic = topic, type = 'text', text = topic } end end return new_topics end local function paste_topic_onto_image (target, topic, x, y, w, h, bg_color, font_name, font_color) assert(target) assert(type(topic) == 'table') assert(type(font_name) == 'string') -- Download and paste found image if topic.type == 'image' then local file_extension = topic.url:match '%.(%a+)$' local url, filename = topic.url, CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.'..file_extension assert(type(url) == 'string' and #url > 0) assert(type(filename) == 'string' and #filename > 0) internet.download_file(url, filename) -- Convert svg to png if url:match '%.svg$' then local filename_2 = CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.'..'png' os.execute('convert -density "1200" -resize 400x400 "'..filename..'" "'..filename_2..'"') filename = filename_2 end -- local found_img = assert(imlib.image.load(filename)) found_img:crop_and_scale(0, 0, found_img:get_width(), found_img:get_height(), w, h) flatten_onto (target, found_img, x, y) found_img:free() --os.remove(filename) elseif topic.type == 'text' then local text = topic.text draw_centered_text_in_box(font_name, target, text, x, y, w, h, bg_color, font_color) elseif topic.type == 'droste' then target:fill_rectangle(x, y, w, h, COLOR_WHITE) else assert(false, topic.type) end end local function copy_remotely (origin_server, origin_path, target_server, target_path) local origin = origin_server and origin_server ~= 'localhost' and origin_server..':'..origin_path or origin_path local target = target_server and target_server ~= 'localhost' and target_server..':'..target_path or target_path os.execute('scp '..origin..' '..target..' > /dev/null') end local function save_to_cloud (img) assert(img) -- local MEME_OUTPUT = CONFIG.IMGGEN_PATH_OUTPUT..'meme.png' img:save (MEME_OUTPUT) img:free() -- Upload to dcav local img_name = 'otmemes_'..os.time()..'.png' copy_remotely('localhost', MEME_OUTPUT, CONFIG.STORAGE_SERVER, CONFIG.STORAGE_SERVER_PATH..img_name) return CONFIG.STORAGE_DIR_URL..img_name end local function draw_droste_effect (target_image, droste_positions, iterations) iterations = interations or DROSTE_ITERATIONS_DEFAULT if #droste_positions <= 0 or iterations <= 0 then return target_image end for _ = 1, iterations do for _, position in ipairs(droste_positions) do local paste_image = target_image:clone() paste_image:crop_and_scale(0, 0, paste_image:get_width(), paste_image:get_height(), position.w, position.h) flatten_onto (target_image, paste_image, position.x, position.y) paste_image:free() end end return target_image end local function droste_positions_from_topics (topics, places) local droste_poss = {} for i, topic in ipairs(topics) do if topic.type == 'droste' then droste_poss[#droste_poss+1] = { x = places[i].x, y = places[i].y, w = places[i].w, h = places[i].h, } end end return droste_poss end local function shallow_copy (t0) local t = {} for k,v in pairs(t0) do t[k] = v end return t end local function generate_comparison_meme_generator (positions) assert(type(positions) == 'table') assert(type(positions.base_img_path) == 'string') return function (topics) assert(type(topics) == 'table' and #topics == #positions) local font_name = choose_random_font() local base_img = imlib.image.load(positions.base_img_path) -- Randomize pos local rand_positions = {} for i, pos in ipairs(positions) do rand_positions[i] = shallow_copy(pos) rand_positions[i].x = pos.x + math.random(-(pos.xr or 0), pos.xr or 0) rand_positions[i].y = pos.y + math.random(-(pos.yr or 0), pos.yr or 0) end -- Paste topic onto head for index, pos in ipairs(rand_positions) do paste_topic_onto_image(base_img, topics[index], pos.x, pos.y, pos.w, pos.h, nil, font_name, pos.font_color) end -- Droste base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, rand_positions)) -- return save_to_cloud(base_img) end end local generate_distracted_boyfriend = generate_comparison_meme_generator { base_img_path = './images/distracted_boyfriend.jpg', { x = 640, xr = 20, y = 50, yr = 20, w = 150, h = 150 }, { x = 180, xr = 20, y = 50, yr = 20, w = 150, h = 150 }, } local generate_drake_egon_olsen = generate_comparison_meme_generator { base_img_path = './images/drake_egon.png', { x = 380, xr = 0, y = 0, yr = 0, w = 380, h = 354 }, { x = 377, xr = 0, y = 354, yr = 0, w = 383, h = 360 }, } local generate_skorsten_image = generate_comparison_meme_generator { base_img_path = './images/bamse_skorsten_1.png', { x = 646, xr = 0, y = 366, yr = 0, w = 631, h = 215, font_color = COLOR_WHITE }, } local GENERATE_COMPARISON_MEME_OF_2 = { generate_distracted_boyfriend, generate_drake_egon_olsen } local BRAIN_ROW_HEIGHT = 150 local BRAIN_ROW_WIDTH = 200 local BRAIN_MAX_EXPLOSION_TOPICS = 6 local BRAIN_DROSTE_POS = {} for i = 1, BRAIN_MAX_EXPLOSION_TOPICS do BRAIN_DROSTE_POS[i] = { x = 0, y = (i-1) * BRAIN_ROW_HEIGHT, w = BRAIN_ROW_WIDTH, h = BRAIN_ROW_HEIGHT } end local function generate_brain_explosion_image (topics) assert(type(topics) == 'table' and 1 <= #topics and #topics <= BRAIN_MAX_EXPLOSION_TOPICS) -- local base_img = imlib.image.new(BRAIN_ROW_WIDTH * 2, BRAIN_ROW_HEIGHT * #topics) base_img:fill_rectangle(0, 0, BRAIN_ROW_WIDTH * 2, BRAIN_ROW_HEIGHT * #topics, COLOR_WHITE) local font_name = choose_random_font() -- for i, topic_info in ipairs(topics) do paste_topic_onto_image(base_img, topic_info, 0, (i-1) * BRAIN_ROW_HEIGHT, BRAIN_ROW_WIDTH, BRAIN_ROW_HEIGHT, COLOR_WHITE, font_name) local brain_img = imlib.image.load(CONFIG.IMGGEN_PATH_BRAINS..'brain_'..i..'.png') brain_img:crop_and_scale(0, 0, brain_img:get_width(), brain_img:get_height(), BRAIN_ROW_WIDTH, BRAIN_ROW_HEIGHT) flatten_onto (base_img, brain_img, BRAIN_ROW_WIDTH, (i-1) * BRAIN_ROW_HEIGHT) brain_img:free() end -- Droste base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, BRAIN_DROSTE_POS)) -- Save return save_to_cloud(base_img) end 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 () local delay = { 0.5, 0.5 } -- Not active if LAST_ACTIVE + ACTIVE_PERIOD < os.time() then delay = { 3, 3 } end -- Sleep sleep(math.random() * delay[2] + delay[1]) -- Set last active LAST_ACTIVE = os.time() end local function is_comparison_message (message) local leq, gep = message:match '<', message:match '>' return leq and not gep and '<' or not leq and gep and '>' end local function reverse (l0) local l, j = {}, #l0 for i = 1, #l0 do l[i], j = l0[j], j - 1 end return l end local COMP_SENTENCES do local ORD_MED_ATTITYDE = { bedre = 'positiv', dårligere = 'negativ', hurtigere = 'positiv', langsommere = 'negativt', robust = 'positivt', } COMP_SENTENCES = {} for ord, attityde in pairs(ORD_MED_ATTITYDE) do local first, second = '{A}', '{B}' if attityde == 'negativ' then first, second = second, first end for _, fmt in ipairs { '%s er %s end %s', '%s er mere %s end %s', '%s er meget %s end %s', '%s %s end %s' } do table.insert(COMP_SENTENCES, fmt:format(first, ord, second)) end end end for sentence_i, sentence in ipairs(COMP_SENTENCES) do local A_pos, B_pos = sentence:find '{A}', sentence:find '{B}' local best_first = (A_pos < B_pos) local pattern_sentence = sentence:gsub('{[AB]}', '(.+)'):gsub('%s+','%%s+') COMP_SENTENCES[pattern_sentence] = { best_first = best_first } COMP_SENTENCES[sentence_i] = nil end local function get_topics_from_comparison_message(message) local comp = is_comparison_message(message) if comp then local topics = {} for topic in message:gmatch('[^'..comp..']+') do local topic_text = clean_text(topic) if topic_text:match '[^%s]' then topics[#topics+1] = topic_text end end -- Rev if comp == '>' then topics = reverse(topics) end -- return (#topics >= 2) and topics or nil end -- Natural language comparisons for pattern, sentence_info in pairs(COMP_SENTENCES) do local good, bad = message:lower():match(pattern) if not sentence_info.best_first then good, bad = bad, good end if good then return { bad, good } end end -- return nil 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 bot:hook('OnJoin', function(user, channel) io.write '...\r'; io.flush() -- if user.nick == CONFIG.IRC_NICK then -- On self join io.write(string.format(' !! Joined %s\n', channel)) 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 and (LAST_TIME_USER_LOGGED_OUT[user.nick] or 0) + IRC_ALLOWED_TIMEOUT < os.time() then -- 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)) end -- io.write ' \r'; io.flush() 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 -- Bait msg if message:match '%f[%a]bait%f[%A]' then human_delay() bot:sendChat(channel, generate_bait_link()) FORRIGE_MEME = os.time() return 'BAIT' end -- Meme machine if message:match '%f[%a]meme%s+machine%f[%A]' then human_delay() bot:sendChat(channel, 'https://www.youtube.com/watch?v=wl-LeTFM8zo') return 'MACHINE' end -- Vælter min skorsten do local problem_text = message:lower():match 'vælter%s+min%s+skorsten%s*(.+)$' if problem_text then if problem_text:match '^er' then problem_text = problem_text:match '^er%s*(.+)$' end if problem_text:match '^:' then problem_text = problem_text:match '^:%s*(.+)$' end local img_link = generate_skorsten_image {{ type = 'text', text = problem_text }} bot:sendChat(channel, img_link) return 'SKRSTEN' end end -- Comparison memes local topics = get_topics_from_comparison_message(message) if not topics then return end local topics = fill_in_topics_information(topics) local img_link, error_message -- if #topics == 2 then img_link, error_message = choose(GENERATE_COMPARISON_MEME_OF_2)(topics) else assert(#topics >= 3) img_link, error_message = generate_brain_explosion_image(topics) end -- if img_link then bot:sendChat(channel, img_link) else bot:sendChat(channel, 'Sorry, no can do. '..tostring(error_message)..'.') end FORRIGE_MEME = os.time() return 'COMPAR' 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) 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