diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/link_token/StandaloneLinkTokenResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/link_token/StandaloneLinkTokenResponse.java index d3871d3..75c990e 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/link_token/StandaloneLinkTokenResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/link_token/StandaloneLinkTokenResponse.java @@ -2,7 +2,7 @@ package nl.andrewl.railsignalapi.rest.dto.link_token; import nl.andrewl.railsignalapi.model.LinkToken; import nl.andrewl.railsignalapi.model.component.Component; -import nl.andrewl.railsignalapi.rest.dto.component.out.SimpleComponentResponse; +import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse; import java.util.Comparator; import java.util.List; @@ -19,7 +19,7 @@ import java.util.List; public record StandaloneLinkTokenResponse ( long id, String label, - List components, + List components, long rsId, String rsName ) { @@ -29,7 +29,7 @@ public record StandaloneLinkTokenResponse ( token.getLabel(), token.getComponents().stream() .sorted(Comparator.comparing(Component::getName)) - .map(SimpleComponentResponse::new).toList(), + .map(ComponentResponse::of).toList(), token.getRailSystem().getId(), token.getRailSystem().getName() ); diff --git a/src/main/java/nl/andrewl/railsignalapi/service/LinkTokenService.java b/src/main/java/nl/andrewl/railsignalapi/service/LinkTokenService.java index 4587dba..0eb58b1 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/LinkTokenService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/LinkTokenService.java @@ -59,6 +59,7 @@ public class LinkTokenService { @Transactional(readOnly = true) public Optional validateToken(String rawToken) { + if (rawToken.length() < LinkToken.PREFIX_SIZE) return Optional.empty(); for (var token : tokenRepository.findAllByTokenPrefix(rawToken.substring(0, LinkToken.PREFIX_SIZE))) { if (passwordEncoder.matches(rawToken, token.getTokenHash())) { return Optional.of(token); diff --git a/src/main/resources/driver/cc/driver.lua b/src/main/resources/driver/cc/driver.lua index 0519441..bdcfe9b 100644 --- a/src/main/resources/driver/cc/driver.lua +++ b/src/main/resources/driver/cc/driver.lua @@ -1,13 +1,16 @@ -- Rail Signal CC:Tweaked Driver local VERSION = "1.0.0" +local args = {...} + -- Global config. Will be loaded at start of script. local config = {} -- Global websocket reference local ws = nil -local function loadConfig() - local configFile = io.open("rs_config.tbl", "r") +-- Loads config from a given filename and returns the table with data. +local function loadConfig(filename) + local configFile = io.open(filename, "r") if not configFile then return nil end @@ -17,6 +20,7 @@ local function loadConfig() return cfg end +-- Fetches JSON data from the given endpoint, using the config's API url. local function fetchJson(endpoint) local response, msg, r = http.get({ url = config.apiUrl .. endpoint, @@ -207,9 +211,20 @@ local function initApiData() end -- MAIN SCRIPT +term.clear() print("Rail Signal Device Driver " .. VERSION .. " for CC:Tweaked computers") print(" By Andrew Lalis ") -config = loadConfig() +print("-------------------------------------------------") +if #args < 1 then + print("Missing required config filename argument.") + return +end +local configFilename = args[1] +config = loadConfig(configFilename) +if config == nil then + print("Error: Could not load config from file.") + return +end print("Loaded config.") if initApiData() then diff --git a/src/main/resources/driver/cc/installer.lua b/src/main/resources/driver/cc/installer.lua new file mode 100644 index 0000000..2bba3af --- /dev/null +++ b/src/main/resources/driver/cc/installer.lua @@ -0,0 +1,222 @@ +local function tableLength(t) + local c = 0 + for _ in pairs(t) do + c = c + 1 + end + return c +end + +local function startsWith(str, s) + return str:find(s, 1, true) == 1 +end + +local function readNum(validationFunction) + local func = validationFunction or function (n) + return n ~= nil, "Please enter a valid number." + end + local num = nil + while true do + local s = io.read() + if s ~= nil then num = tonumber(s) end + local valid, msg = func(num) + if valid then return num else print(msg) end + end +end + +local function readNumInRange(s, e) + return readNum(function (n) + return n ~= nil and n >= s and n <= e, "Please enter a number between " .. s .. " and " .. e .. "." + end) +end + +local function readStr(validationFunction) + local func = validationFunction or function (s) + return s ~= nil and string.len(s) > 0, "Please enter a non-empty string." + end + while true do + local str = io.read() + local valid, msg = func(str) + if valid then return str else print(msg) end + end +end + +local function readUrl() + return readStr(function (s) + return s ~= nil and (startsWith(s, "http://") or startsWith(s, "https://")), "Please enter a valid URL." + end) +end + +local function choice(options, required) + local req = required or false + local maxChoices = tableLength(options) + for i = 1, maxChoices do + local text = options[i].text or options[i] + print("[" .. i .. "] " .. text) + end + + if not req then + maxChoices = maxChoices + 1 + print("[" .. maxChoices .. "] None") + end + local c = readNumInRange(1, maxChoices) + if not req and c == maxChoices then + return nil + else + local value = options[c].value or options[c] + return value + end +end + +local function chooseBoolean() + return choice({"true", "false"}, true) == "true" +end + +local function fetchJson(url) + local response = http.get(url) + if response then + local text = response.readAll() + response.close() + return textutils.unserialiseJSON(text) + else + return nil + end +end + +local function chooseRsSide() + return choice({"front", "back", "left", "right", "top", "bottom"}, true) +end + +local function choosePeripheral(prefix, blacklist) + local ps = peripheral.getNames() + local choices = {} + for _, name in pairs(ps) do + if startsWith(name, prefix) then + local isBanned = false + for _, bannedName in pairs(blacklist) do + if name == bannedName then + isBanned = true + break + end + end + if not isBanned then table.insert(choices, name) end + end + end + if tableLength(choices) < 1 then + return nil + end + return choice(choices, true) +end + +local function configSegmentBoundary(data, sb) + print("What side is the redstone input for this segment boundary?") + data.rsSide = chooseRsSide() + print("Select the detector augment that this segment boundary will use.") + data.augmentId = choosePeripheral("ir_augment_detector", {}) + data.segments = {} + for _, segment in pairs(sb.segments) do + local segmentData = {id = segment.id} + print("In what direction would a train travel towards the segment \"" .. segment.name .. "\"?") + segmentData.direction = string.upper(choice({"North", "South", "East", "West"}, true)) + table.insert(data.segments, segmentData) + end +end + +local function configSignal(data, signal) + print("Select the monitor that this signal will use.") + local monitorId = choosePeripheral("monitor", {}) + data.segment = { + id = signal.segment.id, + monitorId = monitorId + } +end + +local function configSwitch(data, switch) + print("What side is the redstone input/output for this switch?") + data.rsSide = chooseRsSide() + data.possibleConfigurations = {} + for _, cfg in pairs(switch.possibleConfigurations) do + local cfgData = {id = cfg.id} + local names = {} + for _, node in pairs(cfg.nodes) do table.insert(names, node.name) end + local routeName = table.concat(names, ", ") + print("What is the redstone output to configure the switch to send traffic via " .. routeName) + cfgData.rsOutput = chooseBoolean() + table.insert(data.possibleConfigurations, cfgData) + end +end + +-- SCRIPT START + +local config = {} +print("Rail Signal Driver Installer for CC:Tweaked") +print("-------------------------------------------") +print("Please enter the base URL for your Rail System site.") +print(" For example: http://localhost:8080") +local baseUrl = readUrl() +config.apiUrl = baseUrl .. "/api" +if startsWith(baseUrl, "https") then + config.wsUrl = "wss" .. string.sub(baseUrl, 6) .. "/api/ws/component" +else + config.wsUrl = "ws" .. string.sub(baseUrl, 5) .. "/api/ws/component" +end +print("Please enter this device's link token.") +config.linkToken = readStr() +local tokenData = fetchJson(config.apiUrl .. "/lt/" .. config.linkToken) +if tokenData == nil then + print("Error: Could not fetch data for this token. Please make sure your token and URL is correct.") + return +end +print("The token you entered is for the \"" .. tokenData.rsName .. "\" rail system, and controls " .. tableLength(tokenData.components) .. " components:") +for _, component in pairs(tokenData.components) do + print("\t" .. component.name .. "\n\t\tID: " .. component.id .. ", Type: " .. component.type) +end +print("Are you sure you want to continue?\n[1] Continue\n[2] Exit and try again") +local c = readNumInRange(1, 2) +if c == 2 then + return +end + +config.components = {} +for _, component in pairs(tokenData.components) do + print("Configuring component " .. component.name .. ":") + print("-----------------------------------------------") + local componentData = { + id = component.id, + type = component.type + } + if component.type == "SEGMENT_BOUNDARY" then + configSegmentBoundary(componentData, component) + elseif component.type == "SIGNAL" then + configSignal(componentData, component) + elseif component.type == "SWITCH" then + configSwitch(componentData, component) + end + table.insert(config.components, componentData) +end + +print("How often should segment boundaries perform train scans? Give an interval between 0.1 and 3 seconds.") +config.trainScanInterval = readNumInRange(0.1, 3) + +print("Configuration complete!") +local configFile = io.open("__rs_config.tbl", "w") +configFile:write(textutils.serialize(config)) +configFile:close() +print("Saved config.") + +print("Downloading driver...") +local resp = http.get(baseUrl .. "/driver/cc/driver.lua") +local driverScript = resp.readAll() +resp.close() +local driverFile = io.open("rs_driver.lua", "w") +driverFile:write(driverScript) +driverFile:close() +print("Downloaded driver file.") + +local startupFile = io.open("startup.lua", "w") +startupFile:write("shell.execute(\"rs_driver.lua\", \"__rs_config.tbl\")") +startupFile:close() +print("Created startup file.") + +print("Rebooting to start the device...") +os.sleep(1) +os.reboot()