-- 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 local FARVEL_INTERVAL = 90 -- TODO: Invite to bogus channels -------------------------------------------------------------------------------- -- Meme utils local imlib = require 'imlib2' 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 require 'socket' local https = require 'ssl.https' local json = require 'json' local function search_clearbit_for_logo (topic) if not (type(topic) == 'string' and topic == topic:lower() and #topic > 0) then return nil, 'Bad topic: '..tostring(topic) elseif topic:match 'æ' or topic:match 'ø' or topic:match 'å' then return nil, 'Splashbase does not like æøå: '..tostring(topic) end -- for _, domain in ipairs { 'org', 'com', 'net', 'dk' } do local search_url = ('https://logo-core.clearbit.com/%s.%s'):format(topic, domain) local _, code, headers, status = https.request { url = search_url, method = 'HEAD' } if code == 200 then return search_url end end end local function search_splashbase_for_image_topic (topic) if not (type(topic) == 'string' and topic == topic:lower() and #topic > 0) then return nil, 'Bad topic: '..tostring(topic) elseif topic:match 'æ' or topic:match 'ø' or topic:match 'å' then return nil, 'Splashbase does not like æøå: '..tostring(topic) end local search_url = string.format('http://www.splashbase.co/api/v1/images/search?query=%s', topic:gsub('%s', '%%20')) local body, code, headers, status = https.request(search_url) if not body then error(code) end local data = json.decode(body) if not data then return nil, 'JSON could not decode data for '..topic end if #data.images <= 0 then return nil, 'Query returned no data for '..topic end local img_url = data.images[math.random(#data.images)].url assert(type(img_url) == 'string') return img_url end local function download_file (url, filename) -- retrieve the content of a URL local body, code = https.request(url) if not body then error(code) end -- save the content to a file local f = assert(io.open(filename, 'wb')) -- open in "binary" mode f:write(body) f:close() 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 require 'irc.init' local sleep = require 'socket'.sleep --require 'errors' 'memebot' . enable_strict_globals() local function determine_required_font_size (font_name, text, width) 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 end local text_w = font:get_size(text) -- if text_w <= width then size_min = size_guess else size_max = size_guess end end return nil end local COLOR_WHITE = imlib.color.new(255, 255, 255) local COLOR_BLACK = imlib.color.new( 0, 0, 0) local function draw_centered_text_in_box (font_name, draw_onto, text, x0, y0, width, height, bg_color) assert(type(font_name) == 'string') font = determine_required_font_size(font_name, text, width) -- local text_w, text_h = font:get_size(text) local x, y = x0 + (width - text_w) / 2, y0 + (height - text_h) / 2 -- if bg_color then draw_onto:fill_rectangle(x0, y0, width, height, bg_color) end draw_onto:draw_text(font, text, x, y, COLOR_BLACK) 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 CHANCE_OF_GUARENTEED_IMAGE_SEARCH = 0.05 local IMAGE_CHANCE = 0.5 local SCANDI_SYMBOLS = { 'æ', 'Æ', 'ø', 'Ø', 'å', 'Å' } local function should_look_for_images (topic) assert(type(topic) == 'string') if math.random() < CHANCE_OF_GUARENTEED_IMAGE_SEARCH then return true end -- if #topic < 2 then return false end -- for _, symbol in ipairs(SCANDI_SYMBOLS) do if topic:match(symbol) then return false end end -- return math.random() < IMAGE_CHANCE end local function fill_in_topics_information (topics) assert(type(topics) == 'table') -- local new_topics = {} for i, topic in ipairs(topics) do assert(type(topic) == 'string') local topic_l = topic:lower() local url, msg if should_look_for_images(topic_l) then if not url then url, msg = search_clearbit_for_logo (topic_l) end if not url then url, msg = search_splashbase_for_image_topic(topic_l) end end if 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) assert(target) assert(type(topic) == 'table') assert(type(font_name) == 'string') -- Download and paste found image if topic.type == 'image' then local url, filename = topic.url, CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.png' download_file(url, filename) local found_img = 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) 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() copy_remotely('localhost', MEME_OUTPUT, CONFIG.STORAGE_SERVER, CONFIG.STORAGE_SERVER_PATH..img_name..'.png') return CONFIG.STORAGE_DIR_URL..img_name end local function generate_distracted_boyfriend (topics) assert(type(topics) == 'table' and #topics == 2) local font_name = choose_random_font() local base_img = imlib.image.load './images/distracted_boyfriend.jpg' local HEAD_W, HEAD_H = 150, 150 local x1, y1 = 640 + math.random(-20, 20), 50 + math.random(-20, 20) local x2, y2 = 180 + math.random(-20, 20), 50 + math.random(-20, 20) paste_topic_onto_image(base_img, topics[1], x1, y1, HEAD_W, HEAD_H, nil, font_name) paste_topic_onto_image(base_img, topics[2], x2, y2, HEAD_W, HEAD_H, nil, font_name) -- return save_to_cloud(base_img) end local function generate_brain_explosion_image (topics) assert(type(topics) == 'table' and 1 <= #topics and #topics <= 6) -- local ROW_HEIGHT = 150 local ROW_WIDTH = 200 local base_img = imlib.image.new(ROW_WIDTH * 2, ROW_HEIGHT * #topics) 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) * ROW_HEIGHT, ROW_WIDTH, 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(), ROW_WIDTH, ROW_HEIGHT) flatten_onto (base_img, brain_img, ROW_WIDTH, (i-1) * ROW_HEIGHT) brain_img:free() end return save_to_cloud(base_img) 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 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 topics[#topics+1] = clean_text(topic) end -- Rev if comp == '>' then topics = reverse(topics) end -- return topics 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?', } bot:hook('OnJoin', function(user, channel) 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 else -- On other join human_delay() bot:sendChat(channel, 'Hej, '..user.nick..'') end end) local function escape_pattern (text) return text:gsub('[+-?*]', '%%%1') end local FORRIGE_FARVEL = 0 local function handle_message(bot, user, channel, message) -- Direct commands if message:lower():match('^'..escape_pattern(CONFIG.IRC_NICK)..'%f[%A]') then local msg = message:sub(#CONFIG.IRC_NICK+1) if msg:match '%s*join%s+(#%a)' then local channel = msg:match '^%s*join%s+(#%a+)%s*$' bot:sendChat(channel, "Will do! I'll join "..tostring(channel)) bot:join(channel) else bot:sendChat(channel, ERROR_MSG[math.random(#ERROR_MSG)]) end return 'BOT' end -- Bait msg if message:match '%f[%a]bait%f[%A]' then human_delay() bot:sendChat(channel, generate_bait_link()) return 'BAIT' 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 -- 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 and math.random() <= 0.5 then img_link, error_message = generate_distracted_boyfriend(topics) else 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 return 'BRAIN' end bot:hook("OnChat", function(user, channel, message) if channel == CONFIG.IRC_NICK then channel = user.nick end io.write '...\r' io.flush() local success, status = pcall(handle_message, bot, user, channel, message) -- 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) -------------------------------------------------------------------------------- -- Main run local function init_memebot () 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') assert(CONFIG.IRC_CHANNELS == nil or type(CONFIG.IRC_CHANNELS) == 'table') for _, channel in ipairs(CONFIG.IRC_CHANNELS or {}) do bot:join(channel) end while true do bot:think() sleep(0.5) end end -------------------------------------------------------------------------------- if ... then error 'Cannot load Memebot as a library' else init_memebot() end