local serial = require "serialization" local fs = require "filesystem" local bboard = {} local PSYCHOS = _OSVERSION:sub(1,8)=="PsychOS" local f = io.open(PSYCHOS and "/boot/cfg/bboard.cfg" or "/etc/bboard.cfg", "rb") if f then bboard.cfg = serial.unserialize(f:read("*a")) f:close() else bboard.cfg = { path = PSYCHOS and "/boot/bboard" or "/var/spool/bboard", } end local BOARDPATH = string.format("%s/%%s", bboard.cfg.path) local POSTPATH = string.format("%s/%%i", BOARDPATH) local function sanitise(str) return str:gsub("/","") end local flist = PSYCHOS and fs.list or function(path) local rt = {} for file in fs.list(path) do rt[#rt+1] = file end return rt end function bboard.boards() local rt = {} for _, file in ipairs(flist(bboard.cfg.path)) do if fs.isDirectory(string.format(BOARDPATH, file)) then rt[#rt+1] = sanitise(file) end end return rt end function bboard.list(board, start, limit, trunc) assert(type(board) == "string", "invalid board") local rt = {} local ft = flist(string.format(BOARDPATH, board)) local limit, start = math.max(limit or math.huge, 50), #ft table.sort(ft, function(a,b) return a>b end) for k,v in ipairs(ft) do if tonumber(v) and tonumber(v) <= start then rt[#rt+1] = bboard.getPostMeta(board, v, trunc) rt[#rt].id = tonumber(v) end if #rt >= limit then break end end return rt end function bboard.getReplies(board, id, max, skip, trunc) assert(type(board) == "string", "invalid board") assert(type(id) == "number", "invalid post ID") skip=skip or 0 local max = tonumber((bboard.list(board, nil, 1)[1] or {}).id or 0) local rt = {} for i = id, max do if skip < 1 then local post = bboard.getPost(board, i) if post.replyTo == id then rt[#rt+1] = bboard.getPostMeta(board,i,trunc) end if #rt >= math.max(trunc or math.huge, 50) then break end end skip = skip - 1 end return rt end function bboard.getAllReplies(board, id, max, skip, trunc) assert(type(board) == "string", "invalid board") assert(type(id) == "number", "invalid post ID") skip=skip or 0 local max = tonumber((bboard.list(board, nil, 1)[1] or {}).id or 0) local ids,rt = {[id]=true}, {} for i = id, max do if skip < 1 then local post = bboard.getPost(board, i) if ids[post.replyTo] then ids[i], rt[#rt+1] = true, bboard.getPostMeta(board,i,trunc) end if #rt >= math.max(trunc or math.huge, 50) then break end end skip = skip - 1 end return rt end function bboard.getThread(board,id,max,skip,trunc) assert(type(board) == "string", "invalid board") assert(type(id) == "number", "invalid post ID") skip=skip or 0 local op repeat op = bboard.getPostMeta(board, id) id = op.replyTo or id until not op.replyTo return {op, table.unpack(bboard.getAllReplies(board,id,max,skip,trunc))} end function bboard.getPost(board,id) board, id = sanitise(board), tonumber(id) assert(type(board) == "string", "invalid board") assert(type(id) == "number", "invalid post ID") local f, e = io.open(string.format(POSTPATH, board, id), "rb") assert(f, e) local rt = serial.unserialize(f:read("*a")) f:close() rt.date = fs.lastModified(string.format(POSTPATH, board, id)) rt.id = id return rt end function bboard.getPostMeta(board,id,trunc) local rt = bboard.getPost(board,id) rt.content = rt.content:match("^[^\n]+"):sub(1,math.min(trunc or 80, 160)) return rt end function bboard.post(board,post) assert(type(board) == "string", "invalid board") assert(type(post.author) == "string", "invalid author") assert(type(post.subject) == "string" and #post.subject <= 40, "invalid subject") assert(type(post.content) == "string", "no content") post.author = string.format("%s@%s", post.author, os.getenv("RPC_CLIENT") or "localhost") post.id = tonumber((bboard.list(board, nil, 1)[1] or {}).id or 0)+1 local f, e = io.open(string.format(POSTPATH, board, post.id), "wb") assert(f, e) f:write(serial.serialize(post)) f:close() return true end return bboard