local util = {} local magick = require("magick") local db = require("lapis.db") local html_escape = require("lapis.html").escape local constants = require("constants") local string_trim = require("lapis.util").trim local Avatars = require("models").Avatars local Users = require("models").Users local Posts = require("models").Posts local PostHistory = require("models").PostHistory local babycode = require("lib.babycode") util.TransientUser = { is_admin = function (self) return false end, is_mod = function (self) return false end, is_guest = function (self) return true end, is_system = function (self) return false end, is_logged_in_guest = function (self) return false end, is_logged_in = function (self) return false end, username = "Deleted User", } -- PURE API function util.get_user_avatar_url(req, user) return Avatars:find(user.avatar_id).file_path end ---split a string ---@param s string subject ---@param delimiter string? string to split by, can be empty to split by character ---@param max_matches integer? the maximum number of returned elements ---@param trim boolean? whether to trim whitespace off matches ---@param allow_empty boolean? should empty matches be in the resulting table ---@return string[] function util.s_split(s, delimiter, max_matches, trim, allow_empty) local result = {} if s == "" then return result end trim = trim == nil and true or trim local tr = function(subj) if trim then return string_trim(subj) else return subj end end max_matches = max_matches or -1 allow_empty = allow_empty == nil and true or allow_empty if delimiter == "" then for i=1, #s do local c = s:sub(i, 1) if allow_empty or c ~= "" then table.insert(result, c) if max_matches > 0 and #result == max_matches then break end end end return result end local current_pos = 1 local delim_len = #delimiter while true do if max_matches > 0 and #result >= max_matches then break end ---@diagnostic disable-next-line: param-type-mismatch local start_pos, end_pos = s:find(delimiter, current_pos, true) if not start_pos then break end local substr = s:sub(current_pos, start_pos - 1) if allow_empty or substr ~= "" then table.insert(result, tr(substr)) end current_pos = end_pos + 1 end local substr = s:sub(current_pos) if allow_empty or substr ~= "" then table.insert(result, tr(substr)) end return result end function util.split_sentences(sentences, max_sentences) return util.s_split(sentences, ".", max_sentences or 2, true, false) end function util.infobox_message(msg) local sentences = util.split_sentences(msg) if #sentences == 1 then return "" .. sentences[1] .. ". " .. "" end return "" .. sentences[1] .. ". " .. " " .. sentences[2] .. "." end function util.get_logged_in_user(req) if req.session.session_key == nil then return nil end local session = db.select('* FROM "sessions" WHERE "key" = ? AND "expires_at" > "?" LIMIT 1', req.session.session_key, os.time()) if #session > 0 then return Users:find({id = session[1].user_id}) end return nil end function util.get_logged_in_user_or_transient(req) return util.get_logged_in_user(req) or util.TransientUser end function util.ntob(v) return v ~= 0 end function util.bton(b) return 1 and b or 0 end function util.stob(s) if s == "true" then return true end if s == "false" then return false end end function util.form_bool_to_sqlite(s) return util.bton(util.stob(s)) end function util.is_thread_locked(thread) return util.ntob(thread.is_locked) end function util.is_topic_locked(topic) return util.ntob(topic.is_locked) end -- OTHER API function util.validate_and_create_image(input_image, filename) local img = magick.load_image_from_blob(input_image) if not img then return false end img:strip() img:set_gravity("CenterGravity") local width, height = img:get_width(), img:get_height() local min_dim = math.min(width, height) if min_dim > 256 then local ratio = 256.0 / min_dim local new_w, new_h = width * ratio, height * ratio img:resize(new_w, new_h) end width, height = img:get_width(), img:get_height() local crop_size = math.min(width, height) local x_offset = (width - crop_size) / 2 local y_offset = (height - crop_size) / 2 img:crop(crop_size, crop_size, x_offset, y_offset) img:set_format("webp") img:set_quality(85) img:write(filename) img:destroy() return true end function util.destroy_avatar(avatar_id) if avatar_id == 1 then print("won't delete default avatar") return end local avatar = Avatars:find(avatar_id) if not avatar then return end local file_path = "static" .. avatar.file_path local f = io.open(file_path, "r") if not f then print("can't open avatar file") else f:close() os.remove(file_path) avatar:delete() end end function util.create_post(thread_id, user_id, content, markup_language) markup_language = markup_language or "babycode" db.query("BEGIN") local post = Posts:create({ thread_id = thread_id, user_id = user_id, current_revision_id = db.NULL, }) local parsed_content = "" if markup_language == "babycode" then parsed_content = babycode.to_html(content, html_escape) end local revision = PostHistory:create({ post_id = post.id, content = parsed_content, is_initial_revision = true, original_markup = content, markup_language = "babycode", }) post:update({current_revision_id = revision.id}) db.query("COMMIT") return post end function util.transfer_and_delete_user(user) local deleted_user = Users:find({ username = "DeletedUser", }) -- this needs to be atomic db.query("BEGIN") db.query('UPDATE "threads" SET "user_id" = ? WHERE "user_id" = ?', deleted_user.id, user.id) db.query('UPDATE "posts" SET "user_id" = ? WHERE "user_id" = ?', deleted_user.id, user.id) user:delete() -- uncomment later db.query("COMMIT") end function util.pop_infobox(req) if not req.session.infobox then return end req.infobox = req.session.infobox req.session.infobox = nil end function util.inject_infobox(req, message, kind) kind = kind or constants.InfoboxKind.INFO local ib = { msg = message, kind = kind, } req.session.infobox = ib end function util.inject_err_infobox(req, message) local ib = { msg = message, kind = constants.InfoboxKind.ERROR, } req.session.infobox = ib end function util.inject_warn_infobox(req, message) local ib = { msg = message, kind = constants.InfoboxKind.WARN, } req.session.infobox = ib end return util