#!/usr/bin/env lua local posix = require "posix" local usage = [[Usage: obbs [-c config_file] COMMAND [arguments ...] Here COMMAND is one of: newcfg init You can also use the -c option to specify an alternative configuration file. (The default is the file "config.lua" in the current directory.)]] obbs = {} local config_file = "config.lua" local function load_config () io.write("Loading config from file '" .. config_file .. "'... ") dofile(config_file) print("done.") end --- Utility functions --- local function print_table (t, lev) lev = lev or 0 local ind = string.rep("\t", lev) for k,v in pairs(t) do if type(v)=="table" then print(ind ..k .. "= {") print_table(v, lev+1) print(ind .. "}") else print(ind .. k .. "=" .. tostring(v)) end end end local function space_pad(s, n) return string.sub(s,1,n) .. string.rep(" ", math.max(0,n-string.len(s))) end local fs = {} function fs.exists(filename) return io.open(filename, "r") end function fs.dir(path) return posix.dir(path) end function fs.mkdir(dirname) posix.mkdir(dirname) end function fs.rmdir(dirname, force) if force then for i,fname in ipairs(posix.glob(dirname .. "/*")) do posix.unlink(fname) end end assert(posix.rmdir(dirname)) end function fs.copy(filename, destfilename) src = assert(io.open(filename,"r")) dest = assert(io.open(destfilename,"w")) dest:write(src:read("*all")) dest:close() src:close() end function fs.touch(filename) fh = assert(io.open(filename,"w")) fh:close() end function fs.mktempdir() return posix.mkdtemp("/tmp/obbs-XXXXXX") end --- Message parsing --- local msgs = {} function Message(msg) msgs[msg.number] = msg end --- QWK formatting --- local qwk = {} function qwk.write_control(target_dir, obbs, user_name) local cf = assert(io.open(target_dir .. "/CONTROL.DAT", "w")) cf:write(obbs.name .. "\r\n") cf:write("\r\n") -- BBS location cf:write("\r\n") -- BBS phone number cf:write(obbs.sysop .. "\r\n") cf:write("12345," .. obbs.bbsid .. "\r\n") cf:write(os.date("%d-%m-%Y,%X") .. "\r\n") -- packet creation time cf:write(user_name .. "\r\n") cf:write("\r\n") cf:write(0 .. "\r\n") cf:write(0 .. "\r\n") -- TODO: Number of messages in packet cf:write(#obbs.conferences-1 .. "\r\n") -- Index of final conference for i,v in ipairs(obbs.conferences) do cf:write(i-1 .. "\r\n") cf:write(v .. "\r\n") end cf:write("HELLO\r\n") cf:write("NEWS\r\n") cf:write("GOODBYE\r\n") cf:close() end function qwk.write_message (target_dir, obbs) local mf = assert(io.open(target_dir .. "/MESSAGES.DAT", "w")) local pkt_msg_num = 0 mf:write(space_pad("Produced by Qmail...Copyright (c) 1987 by Sparkware." .. " All Rights Reserved", 128)) for cnum,cname in ipairs(obbs.conferences) do msgs = {} dofile(obbs.path .. "/conferences/" .. cname .. ".msgs") for i,msg in ipairs(msgs) do pkt_msg_num = pkt_msg_num + 1 qwk.write_message_header_block(mf, obbs, cnum, pkt_msg_num, msg) qwk.write_message_blocks(mf, obbs, msg) end end mf:close() end function qwk.write_message_header_block(mf, obbs, conf_num, pkt_msg_num, msg) mf:write(" ") -- Message status flag mf:write(space_pad(tostring(msg.number), 7)) mf:write(msg.date) -- mm-dd-yy mf:write(msg.time) -- hh:mm mf:write(space_pad(string.upper(msg.to), 25)) mf:write(space_pad(string.upper(msg.from), 25)) mf:write(space_pad(msg.subject, 25)) mf:write(string.rep(" ", 12)) -- Password (blank) if msg.reply_to then mf:write(space_pad(tostring(msg.reply_to), 8)) else mf:write(string.rep(" ", 8)) end nblocks = math.ceil(string.len(msg.text)/128) + 1 mf:write(space_pad(tostring(nblocks), 6)) -- number of blocks including header mf:write("\xe1") -- flag indicating message is "active" (not "to be killed") mf:write(string.pack("I2",conf_num)) mf:write(string.pack("I2",pkt_msg_num)) mf:write(" ") -- No network tag-line present end function qwk.write_message_blocks(mf, obbs, msg) mf:write((string.gsub(msg.text, "\n", "\xe3"))) local padding = (128 - (string.len(msg.text) % 128)) % 128 mf:write(string.rep("\0",padding)) end --- Commands --- local cmd = {} -- Send default configuration to stdout function cmd.newcfg () print [[ -- OBBS Configuration File obbs.path = "." -- Path to OBBS root obbs.name = "My OBBS Name" obbs.bbsid = "MYOBBSID" -- max 8 char obbs.sysop = "Sysop Name" -- obbs.hello_file = "hello" -- obbs.news_file = "news" -- obbs.goodbye_file = "goodbye" obbs.conferences = { "Announcements", "General", "Meta" } ]] end -- Initialise conference directory structure function cmd.init () load_config() if fs.exists(obbs.path .. "/conferences") or fs.exists(obbs.path .. "/incoming") or fs.exists(obbs.path .. "/outgoing") then print("One or more OBBS directories already exist. Aborting.") return end fs.mkdir(obbs.path .. "conferences") for i,v in ipairs(obbs.conferences) do fs.touch(obbs.path .. "conferences/" .. v .. ".msgs") end fs.mkdir(obbs.path .. "incoming") fs.mkdir(obbs.path .. "outgoing") end -- Pack function cmd.pack () load_config() if not arg[1] then print "Usage: obbs pack USER" return end local user_name = arg[1] local target_dir = fs.mktempdir() -- CONTROL.DAT qwk.write_control(target_dir, obbs, user_name) -- Copy BBS welcome, news and goodbye files if obbs.hello and fs.exists(obbs.hello) then fs.copy(obbs.hello, target_dir .. "/HELLO") else fs.touch(target_dir .. "/HELLO") end if obbs.news and fs.exists(obbs.news) then fs.copy(obbs.news, target_dir .. "/NEWS") else fs.touch(target_dir .. "/NEWS") end if obbs.news and fs.exists(obbs.goodbye) then fs.copy(obbs.goodbye, target_dir .. "/GOODBYE") else fs.touch(target_dir .. "/GOODBYE") end -- Pack messages qwk.write_message(target_dir, obbs) -- Create archive in outgoing os.execute("zip -rj " .. obbs.path .. "/outgoing/" .. obbs.bbsid .. ".qwk " .. target_dir) fs.rmdir(target_dir, true) end -- Main local function main() if #arg > 2 and arg[1]=="-c" then config_file = arg[2] table.remove(arg, 1) table.remove(arg, 1) end if #arg < 1 then print(usage) else if cmd[arg[1]] then local f = cmd[arg[1]] table.remove(arg,1) f() else print("Unknown command '" .. arg[1] .. "'") print(usage) end end end main() .