diff --git a/bbs/OpenOS/bin/bbs.lua b/bbs/OpenOS/bin/bbs.lua new file mode 100644 index 0000000..42b55ff --- /dev/null +++ b/bbs/OpenOS/bin/bbs.lua @@ -0,0 +1,238 @@ +local serial = require "serialization" +local event = require "event" +local rpc = require "rpc" +local _, unicode = pcall(require, "unicode") + +local cfg = {} +local status = "" +local bboard +local ssub = (unicode or {}).sub or string.sub + +local function loadConfig() + local f = io.open("/etc/bbs.cfg","rb") + if not f then return false end + cfg = serial.unserialize(f:read("*a")) + f:close() + return true +end +local function saveConfig() + local f = io.open("/etc/bbs.cfg","wb") + if not f then return false end + f:write(serial.serialize(cfg)) + f:close() + return true +end + +-- figure out terminal geometry + +local function getCursor() + io.write("\27[6n") + local b = "" + repeat + b = b .. io.read(1) + until b:match("\27%[%d+;%d+R") + local y,x = b:match("\27%[(%d+);(%d+)R") + return tonumber(x), tonumber(y) +end +io.write("\27[999;999H") +local mx, my = getCursor() +print(mx, my) + +local function setStatus(str) + local ox, oy = getCursor() + status = str + io.write(string.format("\27[%d;1H\27[0m\27[7m\27[2K%s\27[0m\27[%i;%iH", my, status, oy, ox)) +end + +local function menu(items, state) + local state = state or {1,1} + local i, si = state[1], state[2] + while true do + if si > i - 3 then + si = math.max(1, i-5) + elseif i > (si+my) - 6 then + si = math.min(i - (my-3) + 5, #items - (my - 3)) + end + i = math.max(1,math.min(#items, i)) + print(string.format("\27[H\27[0m\27[7m\27[2K%s\27[0m", items.header or "")) + for j = si, (si+my)-3 do + print((i==j and "\27[7m" or "\27[0m") .. "\27[2K" .. (items[j] or ""):sub(1,mx)) + end + setStatus(status) + local _,_,ch,co = event.pull(60,"key_down") + ch=string.char(ch or 0) + if co == 208 then -- down + i = i + 1 + elseif co == 200 then -- up + i = i - 1 + elseif co == 201 then -- pgdn + i = i - (my - 2) + elseif co == 209 then -- pgdn + i = i + my - 2 + else + return i, ch, co, {i, si} + end + end +end + +local function popup(str,title) + local len = (unicode or {}).len or string.len + if title then + title=string.format("[%s]",title or "") + else + title = "" + end + local width, height, content = 0, 0, {} + for line in str:gmatch("[^\n]*") do + height = height + 1 + width = math.max(width,len(line)) + content[#content+1] = line + end + if width < 1 or height < 1 then return false end + local startx,starty = ((mx//2)-(width//2))-2, ((my//2)-(height//2))-2 + io.write(string.format("\27[%d;%dH╒═%s%s╕",starty,startx,title,("═"):rep((width+1)-len(title)))) + for k,v in pairs(content) do + io.write(string.format("\27[%d;%dH│ %s%s │",starty+k,startx,v,(" "):rep(width-len(v)))) + end + io.write(string.format("\27[%d;%dH┕%s┙",starty+1+#content,startx,("━"):rep(width+2))) +end + +local function query(str, default) + local rv + io.write(string.format(str, default or "no default")) + repeat + rv = io.read() + rv = #rv > 0 and rv or default or "" + until #rv > 0 + return rv +end + +local function help() + popup([[ +[↑][↓] Move 1 entry +[PgUp][PgDn] Move 1 page +[Enter] Select item + +[c] Compose new post +[r] Reply to post +[t] Thread view + +[q] Exit + +Press any key to continue...]],"Usage") + event.pull("key_down") +end + +local function compose(board, post) + local post = post or {} + io.write("\27[2J\27[H") + post.author = cfg.handle + post.subject = query("Subject [%s]? ", post.subject) + fn = os.tmpname() + os.execute(string.format("edit '%s'",fn)) + f = io.open(fn, "rb") + if f then + post.content = f:read("*a") + f:close() + os.execute(string.format("rm '%s'", fn)) + print("Posting...") + bboard.post(board,post) + else + print("Post empty, cancelled.") + end +end + +local function viewPost(board,id) + io.write("\27[2J\27[H") + post = bboard.getPost(board, id) + fn = os.tmpname() + f = io.open(fn, "wb") + f:write(string.format("Author: %s\nSubject: %s\n", post.author, post.subject)) + f:write(post.content) + f:close() + os.execute(string.format("less '%s'", fn)) + os.execute(string.format("rm '%s'", fn)) +end + +local function threadView(board,id) + local run = true + local i, ch, co, st + local list = bboard.getThread(board, id) + while run do + local vl = {header=string.format("%4s %15s %s", "ID", "Author", "Subject")} + status = string.format("[%s] Thread: %s - %i items", cfg.handle, list[1].subject, #list) + for k,v in pairs(list) do + vl[k] = string.format("%4d %15s %s", v.id, v.author:sub(1,15), v.subject):sub(1,mx) + end + i, ch, co, st = menu(vl, st) + if ch == "q" then -- quit + run = false + elseif co == 28 then -- enter, view + viewPost(board, list[i].id) + elseif ch == "r" then -- reply + local prev = list[i] + compose(board, {replyTo=prev.id, subject="Re:"..prev.subject}) + list = bboard.list(board) + elseif ch == "h" or ch == "?" then + help() + end + end +end + +local function boardView(name) + local run = true + local i, ch, co, st, fn, f, post + local list = bboard.list(name) + while run do + status = string.format("[%s] %s/%s - %i items", cfg.handle, cfg.server, name, #list) + local vl = {header=string.format("%4s %15s %s", "ID", "Author", "Subject")} + for k,v in pairs(list) do + vl[k] = string.format("%4d %15s %s", v.id, v.author:sub(1,15), v.subject):sub(1,mx) + end + i, ch, co, st = menu(vl, st) + if ch == "q" then -- quit + run = false + elseif co == 28 then -- enter, view + viewPost(name, list[i].id) + elseif ch == "c" then -- compose + compose(name) + list = bboard.list(name) + elseif ch == "r" then -- reply + local prev = list[i] + compose(name, {replyTo=prev.id, subject="Re:"..prev.subject}) + list = bboard.list(name) + elseif ch == "t" then -- thread view + threadView(name,list[i].id) + elseif ch == "h" or ch == "?" then + help() + end + end +end + +io.write("\27[2J\27[H") +setStatus("Loading config") +loadConfig() +setStatus("Basic setup...") +cfg.server = query("Server [%s]?", cfg.server) +cfg.handle = query("Username? [%s]?", cfg.handle) +setStatus("Saving config") +saveConfig() + +setStatus("Connecting...") +bboard = rpc.proxy(cfg.server, "bbs_") + +local run = true +local i, ch, co, st +while run do + local list = bboard.boards() + setStatus(string.format("Connected to %s - %i boards",cfg.server,#list)) + list.header = "Choose a board..." + i, ch, co, st = menu(list, st) + if ch == "q" then + run = false + elseif co == 28 then + boardView(list[i]) + elseif ch == "h" or ch == "?" then + help() + end +end diff --git a/bbs/OpenOS/etc/rc.d/bbsd.lua b/bbs/OpenOS/etc/rc.d/bbsd.lua new file mode 100644 index 0000000..94db727 --- /dev/null +++ b/bbs/OpenOS/etc/rc.d/bbsd.lua @@ -0,0 +1,17 @@ +local bboard = require "bboard" +local rpc = require "rpc" +function start() + for k,v in pairs(bboard) do + if type(v) == "function" then + rpc.register("bbs_"..k, v) + end + end +end +function stop() + if not rpc.unregister then return false end + for k,v in pairs(bboard) do + rpc.unregister("bbs_"..k) + end +end + +return {start=start, stop=stop} diff --git a/bbs/OpenOS/lib/bboard.lua b/bbs/OpenOS/lib/bboard.lua new file mode 100644 index 0000000..3cd6bd8 --- /dev/null +++ b/bbs/OpenOS/lib/bboard.lua @@ -0,0 +1,140 @@ +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 diff --git a/livefdd/bin/livefdc.lua b/livefdd/bin/livefdc.lua new file mode 100644 index 0000000..b7362a7 --- /dev/null +++ b/livefdd/bin/livefdc.lua @@ -0,0 +1,111 @@ +local serial = require "serialization" +local fs = require "filesystem" + +local tA = {...} + +if #tA < 8 then + print([[livefdc requires 8 arguments, in this order: + - path to clean OpenOS installer + - path to repoinstaller-compatible disk + - comma-separated list of packages to install in the base system + - comma-separated list of packages to archive for live use + - comma-separated list of packages to archive for constrained use + - comma-separated list of extra packages to be unpacked when there is space + - comma-separated list of paths to move into the extra archive + - output path +]]) + os.exit(1) +end + +local function parsecs(str) + local rt = {} + for w in str:gmatch("[^,]+") do + rt[#rt+1] = w + end + return rt +end + +local oospath = fs.canonical(tA[1]) +local pkgpath = fs.canonical(tA[2]) +local basepkg, livepkg, instpkg, extpkg = parsecs(tA[3]), parsecs(tA[4]), parsecs(tA[5]), parsecs(tA[6]) +local archivepaths = parsecs(tA[7]) +local outpath = fs.canonical(tA[8]) + +local f = io.open(pkgpath .. "/master/programs.cfg", "rb") +local pkgs = serial.unserialize(f:read("*a")) +f:close() +local ipkgs,opkgs = {}, {} + +local function run(cmd) + print(cmd) + os.execute(cmd) +end +local function mkdir(path) + print("mkdir",path) + fs.makeDirectory(path) +end +local function copy(from,to) + print("copy",from,to) + fs.copy(from,to) +end +local function move(from,to) + print("move",from,to) + fs.rename(from,to) +end + +local function installPkg(pkgname, dest) + local pkg, opkg = pkgs[pkgname], {} + assert(pkg, "package not available") + print(string.format("Installing %s to %s", pkgname, dest)) + for k,v in pairs(pkg.files or {}) do + v = v:match("^//") and v:sub(2) or fs.concat("/usr", v) + mkdir(fs.concat(dest,v)) + copy(pkgpath .. "/" .. k,fs.concat(dest,v,fs.name(k))) + opkg[k] = fs.concat(v,fs.name(k)) + end + for k,v in pairs(pkg.dependencies or {}) do + if k:match("^https?://") then + v = v:match("^//") and v:sub(2) or fs.concat("/usr", v) + mkdir(fs.concat(dest,v)) + copy(fs.concat(pkgpath, "external", k:match("^https://(.+)$")), fs.concat(dest,v,fs.name(k))) + opkg[k] = fs.concat(v,fs.name(k)) + elseif not ipkgs[k] then + installPkg(k,dest) + end + end + ipkgs[pkgname] = pkg + opkgs[pkgname] = opkg + return true +end + +mkdir(outpath) +run(string.format("cp -rv '%s' '%s/img'",oospath,outpath)) + +for k,v in ipairs(basepkg) do + installPkg(v, fs.concat(outpath, "img")) +end +for k,v in ipairs(livepkg) do + installPkg(v, fs.concat(outpath, "live")) +end +for k,v in ipairs(instpkg) do + installPkg(v, fs.concat(outpath, "inst")) +end +for k,v in ipairs(extpkg) do + installPkg(v, fs.concat(outpath, "extra")) +end + +for k,v in ipairs(archivepaths) do + mkdir(fs.concat(outpath, "extra", fs.path(v))) + move(fs.concat(outpath, "img", v), fs.concat(outpath, "extra", v)) +end + +local f = io.open(fs.concat(outpath, "img", "/etc/opdata.svd"), "wb") +f:write(serial.serialize(opkgs)) +f:close() + +local opwd = os.getenv("PWD") +for k,v in ipairs({"live","inst","extra"}) do + os.setenv("PWD", fs.concat(outpath, v)) + run(string.format("mtar -czv '%s/img/.%s.mtar.lss' *", outpath, v)) +end +os.setenv("PWD",opwd) diff --git a/livefdd/etc/rc.d/livefdd.lua b/livefdd/etc/rc.d/livefdd.lua new file mode 100644 index 0000000..f98cc53 --- /dev/null +++ b/livefdd/etc/rc.d/livefdd.lua @@ -0,0 +1,52 @@ +function start() + +local fs = require "filesystem" +local rootfs = fs.get("/") +local free = rootfs.spaceTotal() - rootfs.spaceUsed() +local function unpack(from,to) + if not fs.exists(from) then return false end + io.write(string.format("Unpacking %s to %s... ", from, to)) + local opwd = os.getenv("PWD") + os.setenv("PWD", to) + os.execute(string.format("mtar -xz '%s'",from)) + os.setenv("PWD", opwd) + print("Done!") +end +local function link(from) + from=fs.canonical(from) + local dt = {from} + for _, dir in ipairs(dt) do + for file in fs.list(dir) do + local fp = fs.concat(dir,file) + if file:match("/$") then + dt[#dt+1] = fp + else + local lt = fp:sub(#from+1) + while not fs.isDirectory(fs.path(lt)) and not fs.isLink(fs.path(lt)) do + lt = fs.path(lt) + end + fs.link(fs.concat(from,lt), lt) + end + end + end +end +if rootfs.isReadOnly() then -- live floppy + print("\27[2J\27[HPreparing live environment...") + fs.makeDirectory("/tmp/live") + unpack("/.live.mtar.lss","/tmp/live") + link("/tmp/live") +elseif not rootfs.isReadOnly() and free < 2^17 then -- installed on small media + print("\27[2J\27[HPreparing constrained environment...") + fs.makeDirectory("/tmp/live") + unpack("/.live.mtar.lss","/tmp/live") + unpack("/.inst.mtar.lss","/tmp/live") + link("/tmp/live") +else -- extract and deactivate + print("\27[2J\27[HFinalising installation...") + unpack("/.live.mtar.lss","/") + unpack("/.inst.mtar.lss","/") + unpack("/.extra.mtar.lss","/") + os.execute("rc livefdd disable") +end + +end