#!/usr/bin/lua5.2
telemver = "0.3 // 2018-05-04"
file = require("pl.file")
io = require("io")
lfs = require("lfs")
os = require("os")
path = require("pl.path")
string = require("string")
table = require("table")
_BBS_ROOT = "/var/bbs/"
bad_topics = {}
bad_topics["."] = true
bad_topics[".."] = true
bad_topics["topic"] = true
-- TODO: This is, obviously, not secure and will need to be updated
username = os.getenv("USER")
current_board = ""
function cat_file(filename)
io.input(filename)
welcome = io.read("*all")
io.input(io.stdin)
io.write(welcome)
end
function show_welcome()
print("::::::::::: This is telem ver."..telemver.." :::::::::::")
cat_file(path.join(_BBS_ROOT, "docs", "welcome"))
end
function show_prompt()
io.write("["..current_board.."] COMMAND :> ")
end
function getchar()
os.execute("/bin/stty -icanon")
local char = io.read(1)
os.execute("/bin/stty icanon")
return char
end
function get_threads(board)
local threads = {}
for topic in lfs.dir(path.join(_BBS_ROOT, "boards", board)) do
if bad_topics[topic] == nil then
local thread = {}
thread.filename = topic
thread.directory = path.join(_BBS_ROOT, "boards", board, topic)
_, _, timestamp, thread.author = string.find(topic, "(%d+)-(%g+)")
thread.timestamp = tonumber(timestamp)
io.input(path.join(thread.directory, "subject"))
thread.subject = io.read("*line")
io.input(io.stdin)
local posts = get_posts(thread)
thread.post_count = #posts
thread.updated = 0
for _, post in ipairs(posts) do
if post.timestamp > thread.updated then
thread.updated = post.timestamp
end
end
table.insert(threads, thread)
end
end
table.sort(threads, function(x,y) return x.updated > y.updated end)
return threads
end
function get_posts(thread)
local posts = {}
for reply in lfs.dir(thread.directory) do
if string.sub(reply, 1,1) == "." then goto continue end
if reply == "subject" then goto continue end
local post = {}
post.filename = path.join(thread.directory, reply)
if reply == "original" then
post.author = thread.author
post.timestamp = thread.timestamp
else
_, _, timestamp, post.author = string.find(reply, "(%d+)-(%a+)")
post.timestamp = tonumber(timestamp)
end
table.insert(posts, post)
::continue::
end
table.sort(posts, function(x,y) return x.timestamp < y.timestamp end)
return posts
end
function do_board()
-- Creates a new (empty) board
-- Get details
io.write("New board name? (Max 18 chars) ")
local board = string.upper(io.read())
if string.len(board) > 18 then
print("Board names must be 18 characters or less!")
return
end
io.write("Description? ")
local desc = io.read()
-- Create directory
local board_dir = path.join(_BBS_ROOT, "boards", board)
lfs.mkdir(board_dir)
-- Write topic file
local topic_file = path.join(board_dir, "topic")
file.write(topic_file, desc)
-- Update representation of BBS
boards[board] = true
table.insert(board_names, board)
table.sort(board_names)
-- Done!
print("Board created.")
end
function do_go()
io.write("Go to which board? ")
local board = string.upper(io.read())
if board == "" then
do_list()
elseif boards[board] == nil then
print("No such board! Hit `l` to list boards.")
else
current_board = board
current_board_threads = get_threads(current_board)
end
end
function do_help()
cat_file(path.join(_BBS_ROOT, "docs", "help"))
end
function do_help2()
cat_file(path.join(_BBS_ROOT, "docs", "help2"))
end
function do_unimplemented()
print("Sorry, this command is not (yet) implemented!")
end
function do_list()
for _,b in pairs(board_names) do
local threads = -3 -- Don't want to count "topic" file or "." or ".."
for topic in lfs.dir(path.join(_BBS_ROOT, "boards", b)) do
threads = threads +1
end
print(string.format("%s [%d threads]", b, threads))
end
end
function do_messages()
if current_board == "" then
print("Not at any board")
else
current_board_threads = get_threads(current_board)
for i, thread in ipairs(current_board_threads) do
print(tostring(i), os.date("%x %H:%M", thread.timestamp), thread.author, thread.subject, "("..tostring(thread.post_count).." posts)")
end
end
end
function do_new()
if current_board == "" then
print("Not at any board!")
return
end
-- Get subject for new thread
io.write("Subject? ")
local subject = io.read()
if string.len(subject) > 48 then
print("Thread subjects must be 48 characters or less!")
return
end
-- Save body of post to temp file
local filename = os.tmpname()
os.execute("$EDITOR " .. filename)
-- TODO: show and confirm
-- Make thread dir
local timestamp = tostring(os.time())
local thread_dir = timestamp .. "-" .. username
local thread_path = path.join(_BBS_ROOT, "boards", current_board, thread_dir)
lfs.mkdir(thread_path)
-- Write subject file
file.write(path.join(thread_path, "subject"),
subject)
-- Move post file
local post_file = thread_dir -- first post and thread directory names are the same!
local newpath = path.join(thread_path, post_file)
local ret, str = file.move(filename, newpath)
if not ret then
print(str)
end
-- Done!
print("Post submitted.")
end
-- Type stuff below
function do_type_first()
current_post_index = 1
do_type_show_post()
end
function do_type_last()
current_post_index = #current_thread_posts
do_type_show_post()
end
function do_type_next()
if current_post_index ~= #current_thread_posts then
current_post_index = current_post_index + 1
end
do_type_show_post()
end
function do_type_prev()
if current_post_index ~= 1 then
current_post_index = current_post_index - 1
end
do_type_show_post()
end
function do_type_reply()
local filename = os.tmpname()
os.execute("$EDITOR " .. filename)
local timestamp = tostring(os.time())
local newfilename = timestamp .. "-" .. username
local newpath = path.join(thread.directory, newfilename)
local ret, str = file.move(filename, newpath)
if not ret then
print(str)
end
current_thread_posts = get_posts(current_thread)
current_post_index = #current_thread_posts
do_type_show_post()
end
type_dispatch = {}
type_dispatch["f"] = do_type_first
type_dispatch["l"] = do_type_last
type_dispatch["n"] = do_type_next
type_dispatch["p"] = do_type_prev
type_dispatch["r"] = do_type_reply
type_dispatch["d"] = function() return end
function do_type()
io.write("Display which thread? ")
local thread_id = tonumber(io.read())
if not thread_id or thread_id < 0 or thread_id > #current_board_threads then
print("Invalid thread index!")
return
end
current_thread = current_board_threads[thread_id]
current_thread_posts = get_posts(current_thread)
current_post_index = #current_thread_posts
do_type_show_post()
repeat
show_type_prompt()
c = getchar()
io.write("\n")
if type_dispatch[c] == nil then
print("Invalid command!")
else
type_dispatch[c]()
end
until c == "d"
end
function show_type_prompt()
io.write("[f]irst, [n]ext, [p]rev, [l]last, [r]eply, [d]one > ")
end
function do_type_show_post()
local post = current_thread_posts[current_post_index]
print("SUBJECT: " .. current_thread.subject)
print("AUTHOR: " .. post.author)
print("POSTED: " .. os.date("%H:%M %B %d, %Y", post.timestamp))
print("--------------------------------")
cat_file(post.filename)
print("--------------------------------")
print(string.format("Viewing post %d of %d in thread", current_post_index, #current_thread_posts))
end
function do_quit()
print("Goodbye!")
end
function do_reply()
if current_board == "" then
print("Not at any board!")
return
end
io.write("Reply to which thread? ")
local thread_id = tonumber(io.read())
if not thread_id or thread_id < 0 or thread_id > #current_board_threads then
print("Invalid thread index!")
return
end
current_thread = current_board_threads[thread_id]
current_thread_posts = get_posts(thread)
current_post_index = #current_thread_posts
do_type_reply()
end
function do_rules()
cat_file(path.join(_BBS_ROOT, "docs", "rules"))
end
function do_stub()
print("Stub!")
end
function do_who()
os.execute("/usr/bin/w")
end
-- MAIN PROGRAM BODY BELOW
show_welcome()
boards, board_names = {}, {}
for board in lfs.dir(path.join(_BBS_ROOT, "boards")) do
if string.sub(board, 1, 1) ~= "." then
boards[board] = true
table.insert(board_names, board)
end
end
table.sort(board_names)
-- Build dispatch table mapping chars to functions
dispatch = {}
dispatch["h"] = do_help
dispatch["g"] = do_go
dispatch["l"] = do_list
dispatch["m"] = do_messages
dispatch["q"] = do_quit
dispatch["t"] = do_type
dispatch["?"] = do_help2
dispatch["!"] = do_rules
dispatch["w"] = do_who
dispatch["s"] = do_unimplemented
dispatch["="] = do_unimplemented
dispatch["M"] = do_unimplemented
dispatch["n"] = do_new
dispatch["r"] = do_reply
dispatch["b"] = do_board
dispatch["c"] = do_unimplemented
-- Infinite loop of command dispatch
repeat
show_prompt()
c = getchar()
io.write("\n")
if dispatch[c] == nil then
print("What?")
else
dispatch[c]()
end
until c == "q"