DorianH935

Untitled

Nov 16th, 2025
45
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.87 KB | None | 0 0
  1. -- lemonserver.lua -- registration & relay server for Lemonphone DM system
  2. -- by ChatGPT (GPT-5)
  3.  
  4. local PROTOCOL = "lemonos-net"
  5. local REGISTRY_FILE = "/lemonserver_registry.db"
  6. local PEERS_FILE = "/lemonserver_peers.db"
  7. local DEFAULT_TTL = 8
  8. local HEARTBEAT_INTERVAL = 30
  9.  
  10. local function save_table(path, tbl)
  11. local f = fs.open(path, "w")
  12. if not f then return false end
  13. f.write(textutils.serialize(tbl))
  14. f.close()
  15. return true
  16. end
  17. local function load_table(path)
  18. if not fs.exists(path) then return {} end
  19. local f = fs.open(path, "r")
  20. if not f then return {} end
  21. local ok, t = pcall(textutils.unserialize, f.readAll())
  22. f.close()
  23. if ok and type(t) == "table" then return t end
  24. return {}
  25. end
  26.  
  27. local registry = load_table(REGISTRY_FILE) -- phone -> { serverID, nodeID, meta, created }
  28. local peers = load_table(PEERS_FILE) -- nodeID string -> true
  29. local seen = {}
  30. local msgCounter = 0
  31. local myID = os.getComputerID()
  32.  
  33. math.randomseed(os.time() + myID)
  34.  
  35. local function mkMsgID()
  36. msgCounter = msgCounter + 1
  37. return tostring(myID) .. "-" .. tostring(os.time()) .. "-" .. tostring(msgCounter)
  38. end
  39.  
  40. local function cleanupSeen()
  41. local now = os.time()
  42. for k,v in pairs(seen) do if v <= now then seen[k] = nil end end
  43. end
  44.  
  45. local function genHex(n)
  46. local s = ""
  47. for i = 1, n do s = s .. string.format("%x", math.random(0,15)) end
  48. return s
  49. end
  50.  
  51. local function makePhone(serviceType)
  52. serviceType = tostring(serviceType or "2")
  53. for _ = 1, 200 do
  54. local a, b = genHex(4), genHex(4)
  55. local num = serviceType .. "-" .. a .. "-" .. b
  56. if not registry[num] then return num end
  57. end
  58. return nil
  59. end
  60.  
  61. local function saveAll()
  62. save_table(REGISTRY_FILE, registry)
  63. save_table(PEERS_FILE, peers)
  64. end
  65.  
  66. local function registerPhone(serviceType, clientNode, clientMeta)
  67. local num = makePhone(serviceType)
  68. if not num then return nil, "exhausted" end
  69. registry[num] = { serverID = myID, nodeID = clientNode, meta = clientMeta or {}, created = os.time() }
  70. saveAll()
  71. return num
  72. end
  73.  
  74. local function sendRaw(node, tbl)
  75. local ok, s = pcall(textutils.serialize, tbl)
  76. if not ok then return false, "serialize" end
  77. rednet.send(tonumber(node), s, PROTOCOL)
  78. return true
  79. end
  80.  
  81. local function broadcastToPeers(tbl)
  82. for k,_ in pairs(peers) do
  83. local nid = tonumber(k)
  84. if nid and nid ~= myID then pcall(sendRaw, nid, tbl) end
  85. end
  86. end
  87.  
  88. local function deliverToClientLocal(localPhone, msg)
  89. local rec = registry[localPhone]
  90. if not rec then return false, "not-local" end
  91. local target = rec.nodeID or rec.serverID
  92. if target then
  93. pcall(sendRaw, tonumber(target), msg)
  94. return true
  95. end
  96. return false, "no-target"
  97. end
  98.  
  99. local function handleRegister(req, fromNode)
  100. local num, err = registerPhone(req.serviceType, fromNode, req.clientInfo)
  101. local res = { msgType = "register_res", id = mkMsgID(), inReplyTo = req.id, assigned = num, timestamp = os.time() }
  102. pcall(sendRaw, fromNode, res)
  103. print(("Registered %s for node %d"):format(tostring(num), fromNode))
  104. end
  105.  
  106. local function handleDM(msg, fromNode)
  107. if not msg.id then return end
  108. if seen[msg.id] then return end
  109. seen[msg.id] = os.time() + 60
  110.  
  111. local rec = registry[msg.toPhone]
  112. if rec and rec.serverID == myID then
  113. local ok, err = deliverToClientLocal(msg.toPhone, msg)
  114. if ok then
  115. local ack = { msgType="ack", id=mkMsgID(), inReplyTo=msg.id, timestamp=os.time(), status="delivered" }
  116. pcall(sendRaw, fromNode, ack)
  117. print(("Delivered DM %s -> %s locally"):format(tostring(msg.fromPhone), tostring(msg.toPhone)))
  118. return
  119. end
  120. end
  121.  
  122. -- known owner but remote
  123. if rec and rec.serverID and rec.serverID ~= myID and peers[tostring(rec.serverID)] then
  124. pcall(sendRaw, tonumber(rec.serverID), msg)
  125. return
  126. end
  127.  
  128. -- relay to peers if unknown or unreachable
  129. if not msg.ttl then msg.ttl = DEFAULT_TTL end
  130. if msg.ttl <= 0 then return end
  131. msg.ttl = msg.ttl - 1
  132. msg.path = msg.path or {}
  133. table.insert(msg.path, myID)
  134. broadcastToPeers(msg)
  135. end
  136.  
  137. local function handleIncoming(raw, sender)
  138. local ok, tbl = pcall(textutils.unserialize, raw)
  139. if not ok or type(tbl) ~= "table" then return end
  140. if tbl.msgType == "register_req" then handleRegister(tbl, sender); return
  141. elseif tbl.msgType == "dm" then handleDM(tbl, sender); return
  142. elseif tbl.msgType == "peer_announce" then peers[tostring(sender)] = true saveAll() return
  143. elseif tbl.msgType == "heartbeat" then peers[tostring(sender)] = true saveAll() return
  144. end
  145. end
  146.  
  147. local function receiveLoop()
  148. while true do
  149. local sender, raw = rednet.receive(PROTOCOL)
  150. if sender and raw then
  151. local ok, err = pcall(handleIncoming, raw, sender)
  152. if not ok then print("Error handling: " .. tostring(err)) end
  153. end
  154. end
  155. end
  156.  
  157. local function housekeeping()
  158. while true do
  159. cleanupSeen()
  160. local announce = { msgType = "peer_announce", id = mkMsgID(), timestamp = os.time(), serverID = myID }
  161. broadcastToPeers(announce)
  162. sleep(HEARTBEAT_INTERVAL)
  163. end
  164. end
  165.  
  166. local function adminConsole()
  167. print("LemonServer admin console. Commands: peers, registry, addpeer <id>, rempeer <id>, save, quit")
  168. while true do
  169. io.write("> ")
  170. local line = io.read()
  171. if not line then break end
  172. local cmd, arg = line:match("^(%S+)%s*(.*)$")
  173. if cmd == "peers" then
  174. print("Peers:")
  175. for k,_ in pairs(peers) do print(" - " .. k) end
  176. elseif cmd == "registry" then
  177. print("Registry entries:")
  178. for p,v in pairs(registry) do
  179. print((" %s -> server %s node %s (created %s)"):format(p, tostring(v.serverID), tostring(v.nodeID), tostring(v.created)))
  180. end
  181. elseif cmd == "addpeer" and arg ~= "" then
  182. peers[arg] = true saveAll() print("Added " .. arg)
  183. elseif cmd == "rempeer" and arg ~= "" then
  184. peers[arg] = nil saveAll() print("Removed " .. arg)
  185. elseif cmd == "save" then
  186. saveAll() print("Saved")
  187. elseif cmd == "quit" then print("Exiting console.") break
  188. else print("Unknown command.")
  189. end
  190. end
  191. end
  192.  
  193. -- start server
  194. if peripheral.find("modem") then
  195. -- best-effort open modem on a side; prefer existing config
  196. -- open all modem sides to be safe if possible
  197. for _, side in ipairs(peripheral.getNames()) do
  198. if peripheral.getType(side) == "modem" then
  199. pcall(rednet.open, side)
  200. end
  201. end
  202. end
  203.  
  204. print("LemonServer starting as node " .. tostring(myID))
  205. saveAll()
  206. parallel.waitForAny(receiveLoop, housekeeping, adminConsole)
  207.  
Advertisement
Add Comment
Please, Sign In to add comment