memebot/main.lua

850 lines
27 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
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
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
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