#!/usr/bin/env lua
--- OBBS: An Offline-first BBS Utility
+-- QWiKBoard: A QWK-powered message board system
-- Copyright (C) 2024 plugd
-- This program is free software: you can redistribute it and/or modify
local posix = require "posix"
-local usage = [[Usage: obbs [-c config_file] COMMAND arguments
+local usage = [[Usage: qwikboard DIR COMMAND arguments
-Here COMMAND is one of:
+Here DIR is the path to the message board directory and COMMAND is one of:
newcfg
init
qwkout
repin
nncp_handler
+]]
-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"
+qb = {}
local function load_config ()
+ local config_file = qb.path .. "/config.lua"
io.write("Loading config from file '" .. config_file .. "'... ")
dofile(config_file)
print("done.")
return io.open(filename, "r")
end
+function fs.direxists(dirname)
+ return pcall(posix.dir, dirname)
+end
+
function fs.dir(path)
return posix.dir(path)
end
end
function fs.mktempdir()
- return posix.mkdtemp("/tmp/obbs-XXXXXX")
+ return posix.mkdtemp("/tmp/qwikboard-XXXXXX")
end
--- Message database parsing ---
msgs[msg.number] = msg
end
-local function get_next_msg_number(obbs, conf_num)
- local msgnum_filename = obbs.path .. "/conferences/" ..
- obbs.conferences[conf_num+1] .. ".next"
+local function get_next_msg_number(conf_num)
+ local msgnum_filename = qb.path .. "/conferences/" ..
+ qb.conferences[conf_num+1] .. ".next"
local mnf = assert(io.open(msgnum_filename, "r"))
local next_num = tonumber(mnf:read("*all"))
mnf:close()
return next_num
end
-local function set_next_msg_number(obbs, conf_num, next_num)
- local msgnum_filename = obbs.path .. "/conferences/" ..
- obbs.conferences[conf_num+1] .. ".next"
+local function set_next_msg_number(conf_num, next_num)
+ local msgnum_filename = qb.path .. "/conferences/" ..
+ qb.conferences[conf_num+1] .. ".next"
local mnf = assert(io.open(msgnum_filename, "w"))
mnf:write(next_num)
mnf:close()
end
-local function append_new_msg(obbs, conf_num, msg)
- local cf = assert(io.open(obbs.path .. "/conferences/" ..
- obbs.conferences[conf_num+1] .. ".msgs", "a+"))
+local function append_new_msg(conf_num, msg)
+ local cf = assert(io.open(qb.path .. "/conferences/" ..
+ qb.conferences[conf_num+1] .. ".msgs", "a+"))
cf:write("\n")
cf:write("Message{\n")
cf:write("\tnumber=" .. msg.number .. ",\n")
local qwk = {}
-function qwk.write_control(target_dir, obbs, user_name)
+function qwk.write_control(target_dir, user_name)
local cf = assert(io.open(target_dir .. "/CONTROL.DAT", "w"))
- cf:write(obbs.name .. "\r\n")
+ cf:write(qb.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(qb.sysop .. "\r\n")
+ cf:write("12345," .. qb.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(#qb.conferences-1 .. "\r\n") -- Index of final conference
+ for i,v in ipairs(qb.conferences) do
cf:write(i-1 .. "\r\n")
cf:write(v .. "\r\n")
end
cf:close()
end
-function qwk.write_message (target_dir, obbs)
+function qwk.write_message (target_dir)
local mf = assert(io.open(target_dir .. "/MESSAGES.DAT", "w"))
local pkt_msg_num = 0
- mf:write(space_pad("Produced by OBBS.", 128))
+ mf:write(space_pad("Produced by QWiKBoard.", 128))
- for cnum,cname in ipairs(obbs.conferences) do
+ for cnum,cname in ipairs(qb.conferences) do
msgs = {}
- dofile(obbs.path .. "/conferences/" .. cname .. ".msgs")
+ dofile(qb.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_text(mf, obbs, msg)
+ qwk.write_message_header_block(mf, qb, cnum, pkt_msg_num, msg)
+ qwk.write_message_text(mf, qb, msg)
end
end
mf:close()
end
-function qwk.write_message_header_block(mf, obbs, conf_num, pkt_msg_num, msg)
+function qwk.write_message_header_block(mf, 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(" ") -- No network tag-line present
end
-function qwk.write_message_text(mf, obbs, msg)
+function qwk.write_message_text(mf, 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
-function qwk.process_replies(archive_dir, obbs, user_name)
+function qwk.process_replies(archive_dir, user_name)
- local msg_filename = archive_dir .. "/" .. obbs.bbsid .. ".MSG"
+ local msg_filename = archive_dir .. "/" .. qb.bbsid .. ".MSG"
if not fs.exists(msg_filename) then
print("Error: MSG file not found.")
local mf = assert(io.open(msg_filename, "r"))
local block = mf:read(128)
- if not string.find(block, obbs.bbsid .. " *") then
+ if not string.find(block, qb.bbsid .. " *") then
print("Error: Failed to match BBSID in first block.")
return false
end
while mf:read(0) do
- qwk.process_reply(mf, obbs, user_name)
+ qwk.process_reply(mf, qb, user_name)
end
end
-function qwk.process_reply(mf, obbs, user_name)
+function qwk.process_reply(mf, user_name)
local msg = {}
mf:read(1) -- Message status (ignore for now)
end
msg.text = string.gsub(msg.text, "\xe3", "\n")
- msg.number = get_next_msg_number(obbs, conf_num)
- set_next_msg_number(obbs, conf_num, msg.number+1)
+ msg.number = get_next_msg_number(qb, conf_num)
+ set_next_msg_number(qb, conf_num, msg.number+1)
- append_new_msg(obbs, conf_num, msg)
+ append_new_msg(qb, conf_num, msg)
- print("Processed message for conference " .. obbs.conferences[conf_num+1] .. ".")
+ print("Processed message for conference " .. qb.conferences[conf_num+1] .. ".")
-- print("Message{")
-- for k,v in pairs(msg) do
-- Send default configuration to stdout
function cmd.newcfg ()
- print [[
--- OBBS Configuration File
-obbs.path = "." -- Path to OBBS root
+ local config_file = qb.path .. "/config.lua"
+
+ if fs.exists(config_file) then
+ print("Configuration file '" .. config_file .. "' already exists. Aborting.")
+ return false
+ end
+
+ print("Writing new configuration file " .. config_file .. "...")
-obbs.name = "My OBBS Name"
-obbs.bbsid = "MYOBBSID" -- upper case, max 8 char
-obbs.sysop = "Sysop Name"
+ local cf = assert(io.open(config_file, "w"))
+ cf:write([[
+-- QWiKBoard Configuration File
-obbs.conferences = {
+qb.name = "My BBS Name"
+qb.bbsid = "MYBBSID" -- upper case, max 8 char
+qb.sysop = "Sysop Name"
+
+qb.conferences = {
"Announcements",
"General",
"Meta"
}
-- Ensure these point to zip and unzip on your system
-obbs.zip = "/usr/bin/zip"
-obbs.unzip = "/usr/bin/unzip"
-]]
+qb.zip = "/usr/bin/zip"
+qb.unzip = "/usr/bin/unzip"
+]])
+ cf:close()
+
end
-- Initialise conference directory structure
function cmd.init ()
load_config()
- if fs.exists(obbs.path .. "/conferences") or
- fs.exists(obbs.path .. "/notices") then
- print("One or more OBBS directories already exist. Aborting.")
+ if fs.exists(qb.path .. "/conferences") or
+ fs.exists(qb.path .. "/notices") then
+ print("One or more QWiKBoard directories already exist. Aborting.")
return false
end
- fs.mkdir(obbs.path .. "/conferences")
- for i,v in ipairs(obbs.conferences) do
- fs.touch(obbs.path .. "/conferences/" .. v .. ".msgs")
- io.open(obbs.path .. "/conferences/" .. v .. ".next", "w"):write(1)
+ fs.mkdir(qb.path .. "/conferences")
+ for i,v in ipairs(qb.conferences) do
+ fs.touch(qb.path .. "/conferences/" .. v .. ".msgs")
+ io.open(qb.path .. "/conferences/" .. v .. ".next", "w"):write(1)
end
- fs.mkdir(obbs.path .. "/notices")
- fs.touch(obbs.path .. "/notices/hello")
- fs.touch(obbs.path .. "/notices/news")
- fs.touch(obbs.path .. "/notices/goodbye")
+ fs.mkdir(qb.path .. "/notices")
+ fs.touch(qb.path .. "/notices/hello")
+ fs.touch(qb.path .. "/notices/news")
+ fs.touch(qb.path .. "/notices/goodbye")
end
-- qwkout: generate QWK file
load_config()
if not arg[2] then
- print [[Usage: obbs qwkout USER OUTFILE
+ print [[Usage: qwikboard DIR qwkout USER OUTFILE
Here OUTFILE is is the name of the output QWK file relative to
the current directory. The QWK file is traditionally named
local work_dir = fs.mktempdir()
-- CONTROL.DAT
- qwk.write_control(work_dir, obbs, user_name)
+ qwk.write_control(work_dir, qb, user_name)
-- Copy BBS welcome, news and goodbye files
- fs.copy(obbs.path .. "/notices/hello", work_dir .. "/HELLO")
- fs.copy(obbs.path .. "/notices/news", work_dir .. "/NEWS")
- fs.copy(obbs.path .. "/notices/goodbye", work_dir .. "/GOODBYE")
+ fs.copy(qb.path .. "/notices/hello", work_dir .. "/HELLO")
+ fs.copy(qb.path .. "/notices/news", work_dir .. "/NEWS")
+ fs.copy(qb.path .. "/notices/goodbye", work_dir .. "/GOODBYE")
-- Pack messages
- qwk.write_message(work_dir, obbs)
+ qwk.write_message(work_dir, qb)
-- Create archive in outgoing
- os.execute(obbs.zip .. " -rj " ..
- obbs.path .. "/" .. qwk_filename ..
+ os.execute(qb.zip .. " -rj " ..
+ qb.path .. "/" .. qwk_filename ..
" " .. work_dir)
fs.rmdir(work_dir, true)
load_config()
if not arg[2] then
- print [[Usage: obbs repin INFILE USER
+ print [[Usage: qwikboard DIR repin INFILE USER
Here INFILE is the name of the REP file to import new messages
from. This is traditionally named BBSID.rep, where BBSID is
-the 8 character max ID of the BBS, but obbs doesn't care about this.]]
+the 8 character max ID of the BBS, but QWiKBoard doesn't care
+about this.]]
return
end
local work_dir = fs.mktempdir()
fs.copy(rep_filename, work_dir .. "/repfile.rep")
- os.execute(obbs.unzip .. " " .. work_dir .. "/repfile.rep -d " .. work_dir)
+ os.execute(qb.unzip .. " " .. work_dir .. "/repfile.rep -d " .. work_dir)
- qwk.process_replies(work_dir, obbs, user_name)
+ qwk.process_replies(work_dir, qb, user_name)
fs.rmdir(work_dir, true)
to their neigh entry in the /etc/nncp.hjson file
exec: {
- obbs: ["/usr/bin/obbs -c /var/share/obbs/config.lua nncp"]
+ offline: ["/usr/local/bin/qwikboard /var/qb/ nncp_handler"]
}]]
+ end
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
+ if #arg < 2 then
print(usage)
else
- if cmd[arg[1]] then
- local f = cmd[arg[1]]
- table.remove(arg,1)
- f()
+ if not fs.direxists(arg[1]) then
+ print("Directory '" .. arg[1] .. "' does not exist. Aborting.")
else
- print("Unknown command '" .. arg[1] .. "'")
- print(usage)
+ qb.path = arg[1]
+ table.remove(arg, 1)
+ 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
end