Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- lemonserver.lua -- registration & relay server for Lemonphone DM system
- -- by ChatGPT (GPT-5)
- local PROTOCOL = "lemonos-net"
- local REGISTRY_FILE = "/lemonserver_registry.db"
- local PEERS_FILE = "/lemonserver_peers.db"
- local DEFAULT_TTL = 8
- local HEARTBEAT_INTERVAL = 30
- local function save_table(path, tbl)
- local f = fs.open(path, "w")
- if not f then return false end
- f.write(textutils.serialize(tbl))
- f.close()
- return true
- end
- local function load_table(path)
- if not fs.exists(path) then return {} end
- local f = fs.open(path, "r")
- if not f then return {} end
- local ok, t = pcall(textutils.unserialize, f.readAll())
- f.close()
- if ok and type(t) == "table" then return t end
- return {}
- end
- local registry = load_table(REGISTRY_FILE) -- phone -> { serverID, nodeID, meta, created }
- local peers = load_table(PEERS_FILE) -- nodeID string -> true
- local seen = {}
- local msgCounter = 0
- local myID = os.getComputerID()
- math.randomseed(os.time() + myID)
- local function mkMsgID()
- msgCounter = msgCounter + 1
- return tostring(myID) .. "-" .. tostring(os.time()) .. "-" .. tostring(msgCounter)
- end
- local function cleanupSeen()
- local now = os.time()
- for k,v in pairs(seen) do if v <= now then seen[k] = nil end end
- end
- local function genHex(n)
- local s = ""
- for i = 1, n do s = s .. string.format("%x", math.random(0,15)) end
- return s
- end
- local function makePhone(serviceType)
- serviceType = tostring(serviceType or "2")
- for _ = 1, 200 do
- local a, b = genHex(4), genHex(4)
- local num = serviceType .. "-" .. a .. "-" .. b
- if not registry[num] then return num end
- end
- return nil
- end
- local function saveAll()
- save_table(REGISTRY_FILE, registry)
- save_table(PEERS_FILE, peers)
- end
- local function registerPhone(serviceType, clientNode, clientMeta)
- local num = makePhone(serviceType)
- if not num then return nil, "exhausted" end
- registry[num] = { serverID = myID, nodeID = clientNode, meta = clientMeta or {}, created = os.time() }
- saveAll()
- return num
- end
- local function sendRaw(node, tbl)
- local ok, s = pcall(textutils.serialize, tbl)
- if not ok then return false, "serialize" end
- rednet.send(tonumber(node), s, PROTOCOL)
- return true
- end
- local function broadcastToPeers(tbl)
- for k,_ in pairs(peers) do
- local nid = tonumber(k)
- if nid and nid ~= myID then pcall(sendRaw, nid, tbl) end
- end
- end
- local function deliverToClientLocal(localPhone, msg)
- local rec = registry[localPhone]
- if not rec then return false, "not-local" end
- local target = rec.nodeID or rec.serverID
- if target then
- pcall(sendRaw, tonumber(target), msg)
- return true
- end
- return false, "no-target"
- end
- local function handleRegister(req, fromNode)
- local num, err = registerPhone(req.serviceType, fromNode, req.clientInfo)
- local res = { msgType = "register_res", id = mkMsgID(), inReplyTo = req.id, assigned = num, timestamp = os.time() }
- pcall(sendRaw, fromNode, res)
- print(("Registered %s for node %d"):format(tostring(num), fromNode))
- end
- local function handleDM(msg, fromNode)
- if not msg.id then return end
- if seen[msg.id] then return end
- seen[msg.id] = os.time() + 60
- local rec = registry[msg.toPhone]
- if rec and rec.serverID == myID then
- local ok, err = deliverToClientLocal(msg.toPhone, msg)
- if ok then
- local ack = { msgType="ack", id=mkMsgID(), inReplyTo=msg.id, timestamp=os.time(), status="delivered" }
- pcall(sendRaw, fromNode, ack)
- print(("Delivered DM %s -> %s locally"):format(tostring(msg.fromPhone), tostring(msg.toPhone)))
- return
- end
- end
- -- known owner but remote
- if rec and rec.serverID and rec.serverID ~= myID and peers[tostring(rec.serverID)] then
- pcall(sendRaw, tonumber(rec.serverID), msg)
- return
- end
- -- relay to peers if unknown or unreachable
- if not msg.ttl then msg.ttl = DEFAULT_TTL end
- if msg.ttl <= 0 then return end
- msg.ttl = msg.ttl - 1
- msg.path = msg.path or {}
- table.insert(msg.path, myID)
- broadcastToPeers(msg)
- end
- local function handleIncoming(raw, sender)
- local ok, tbl = pcall(textutils.unserialize, raw)
- if not ok or type(tbl) ~= "table" then return end
- if tbl.msgType == "register_req" then handleRegister(tbl, sender); return
- elseif tbl.msgType == "dm" then handleDM(tbl, sender); return
- elseif tbl.msgType == "peer_announce" then peers[tostring(sender)] = true saveAll() return
- elseif tbl.msgType == "heartbeat" then peers[tostring(sender)] = true saveAll() return
- end
- end
- local function receiveLoop()
- while true do
- local sender, raw = rednet.receive(PROTOCOL)
- if sender and raw then
- local ok, err = pcall(handleIncoming, raw, sender)
- if not ok then print("Error handling: " .. tostring(err)) end
- end
- end
- end
- local function housekeeping()
- while true do
- cleanupSeen()
- local announce = { msgType = "peer_announce", id = mkMsgID(), timestamp = os.time(), serverID = myID }
- broadcastToPeers(announce)
- sleep(HEARTBEAT_INTERVAL)
- end
- end
- local function adminConsole()
- print("LemonServer admin console. Commands: peers, registry, addpeer <id>, rempeer <id>, save, quit")
- while true do
- io.write("> ")
- local line = io.read()
- if not line then break end
- local cmd, arg = line:match("^(%S+)%s*(.*)$")
- if cmd == "peers" then
- print("Peers:")
- for k,_ in pairs(peers) do print(" - " .. k) end
- elseif cmd == "registry" then
- print("Registry entries:")
- for p,v in pairs(registry) do
- print((" %s -> server %s node %s (created %s)"):format(p, tostring(v.serverID), tostring(v.nodeID), tostring(v.created)))
- end
- elseif cmd == "addpeer" and arg ~= "" then
- peers[arg] = true saveAll() print("Added " .. arg)
- elseif cmd == "rempeer" and arg ~= "" then
- peers[arg] = nil saveAll() print("Removed " .. arg)
- elseif cmd == "save" then
- saveAll() print("Saved")
- elseif cmd == "quit" then print("Exiting console.") break
- else print("Unknown command.")
- end
- end
- end
- -- start server
- if peripheral.find("modem") then
- -- best-effort open modem on a side; prefer existing config
- -- open all modem sides to be safe if possible
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem" then
- pcall(rednet.open, side)
- end
- end
- end
- print("LemonServer starting as node " .. tostring(myID))
- saveAll()
- parallel.waitForAny(receiveLoop, housekeeping, adminConsole)
Advertisement
Add Comment
Please, Sign In to add comment