commit 39067f750edede2b325471756f69fec7a8167061
Author: Katja Ramona Sophie Kwast (zaphyra) <git@zaphyra.eu>
Date: Sat, 7 Nov 2026 21:41:01 +0100
Author: Katja Ramona Sophie Kwast (zaphyra) <git@zaphyra.eu>
Date: Sat, 7 Nov 2026 21:41:01 +0100
init
8 files changed, 311 insertions(+), 0 deletions(-)
A
|
193
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/flake.lock b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1793865203, + "narHash": "sha256-/cpdPHaySDIyCgZI6EWngiM6jK3YLUNzv1b7lTzy6Bk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c56a87d8e41ce69bd2af57eb35dcc117a9dfca33", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +}
diff --git a/flake.nix b/flake.nix @@ -0,0 +1,42 @@ +{ + + description = ""; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + + outputs = + inputs: + let + forAllSystems = + function: + inputs.nixpkgs.lib.genAttrs + [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + ] + ( + system: + function ( + import inputs.nixpkgs { + system = system; + overlays = [ inputs.self.overlays.default ]; + } + ) + ); + + in + { + + packages = forAllSystems (pkgs: { + default = pkgs.pebble-foo; + pebble-foo = pkgs.pebble-foo; + }); + + overlays.default = final: prev: { + pebble-foo = prev.callPackage ./nix/package.nix { }; + }; + + }; + +}
diff --git a/lock.json b/lock.json @@ -0,0 +1,16 @@ +{ + "depends": [ + { + "method": "fetchzip", + "path": "/nix/store/sv8ydzdf509akbm3b93qlppklgvlx40x-source", + "rev": "5ffa3021d78e79f18482192548b8bd835002f7ab", + "sha256": "0h3nxnjclqd1gn9q3p9lad4i6ypwdisnbqj6rafm9155ydkc724v", + "url": "https://github.com/nim-lang/htmlparser/archive/5ffa3021d78e79f18482192548b8bd835002f7ab.tar.gz", + "ref": "v0.1.0", + "packages": [ + "htmlparser" + ], + "srcDir": "src" + } + ] +}
diff --git a/nix/package.nix b/nix/package.nix @@ -0,0 +1,19 @@ +{ + buildNimPackage, + openssl, + sqlite, +}: + +buildNimPackage { + name = "pebble-foo"; + src = ../.; + + lockFile = ../lock.json; + + buildInputs = [ + openssl + sqlite + ]; + + nimRelease = true; +}
diff --git a/pebble-foo.nimble b/pebble-foo.nimble @@ -0,0 +1,12 @@ +# Package + +version = "0.1.0" +author = "katja (zaphyra)" +description = "pebble proxy stuff" +license = "MIT" +srcDir = "src" +bin = @[ "risProxy" ] + +# Dependencies +requires "nim >= 1.6.0" +requires "htmlparser >= 0.1.0"
diff --git a/src/risProxy.nim b/src/risProxy.nim @@ -0,0 +1,193 @@ +import std/[httpclient, cookies, uri, files, paths, syncio, cgi, times, strutils, tables, json] +import std/math +import std/times +import pkg/htmlparser +import std/xmltree # To use '$' for XmlNode +import std/strtabs # To access XmlAttributes +import std/strutils # To use cmpIgnoreCase + +var tokenFile = Path("/tmp/csrf-token") +var csrf: JsonNode + +var dbregioClient = newHttpClient() + +var verkehrslageClient = newHttpClient() +verkehrslageClient.headers["db-api-key"] = "32a84ba3-de45-46b3-a580-e3cac9591981" +verkehrslageClient.headers["db-client-id"] = "ris-api" + +proc calcDistMeters(lat1, lon1, lat2, lon2: float): float = + var R = 6371000.0; + var dLat = ((lat2 - lat1) * PI) / 180 + var dLon = ((lon2 - lon1) * PI) / 180 + var a = + sin(dLat / 2) * sin(dLat / 2) + + cos((lat1 * PI) / 180) * + cos((lat2 * PI) / 180) * + sin(dLon / 2) * + sin(dLon / 2); + result = ( + R * 2.0 * arctan2( + sqrt(a), + sqrt(1 - a) + ) + ) + +proc getCsrfToken() = + if not fileExists(tokenFile): + var response = dbregioClient.request("https://wifi-landingpage.dbregio.de/", HttpGet) + var html = parseHtml(response.bodyStream) + var cookieJar = parseCookies(response.headers["set-cookie"]) + + for meta in html.findAll("meta"): + if (meta.attrs.hasKey "name") and (meta.attrs["name"] == "csrf-token"): + csrf = %* { + "token": meta.attrs["content"], + "cookie": cookieJar["__Host-csrf"], + } + writeFile($tokenFile, $csrf) + else: + csrf = parseFile($tokenFile) + + dbregioClient.headers["Cookie"] = "__Host-csrf=" & $csrf["cookie"] + dbregioClient.headers["Csrf-Token"] = $csrf["token"] + +proc risStationsRequest(endpoint: string, params: openArray[(string, string)]): JsonNode = + var response = verkehrslageClient.request(parseUri("https://verkehrslage.bahnhof.de/api/stations") / endpoint ? params) + result = parseJson(response.body) + +proc risBoardsRequest(endpoint: string, params: openArray[(string, string)]): JsonNode = + var response = dbregioClient.request(parseUri("https://wifi-landingpage.dbregio.de/api/boards") / endpoint ? params) + + if response.code != Http200: + removeFile(tokenFile) + getCsrfToken() + response = dbregioClient.request(parseUri("https://wifi-landingpage.dbregio.de/api/boards") / endpoint ? params) + + result = parseJson(response.body) + +proc risJourneysRequest(id: string, params: openArray[(string, string)]): JsonNode = + var response = dbregioClient.request(parseUri("https://wifi-landingpage.dbregio.de/api/journeys") / id ? params) + + if response.code != Http200: + removeFile(tokenFile) + getCsrfToken() + response = dbregioClient.request(parseUri("https://wifi-landingpage.dbregio.de/api/journeys") / id ? params) + + result = parseJson(response.body) + +proc getStations(lat, lon: float, limit: int = 3, radius: int = 500): JsonNode = + var response = risStationsRequest("/stop-places/by-position", @[ + ("groupBy", "SALES"), + ("latitude", $lat), + ("longitude", $lon), + ("radius", $radius), + ("limit", $limit), + ]) + + result = newJArray() + for station in items(response["stopPlaces"]): + var name: string + if station["names"]["DE"].hasKey("speechShort"): + name = station["names"]["DE"]["speechShort"].getStr() + else: + name = station["names"]["DE"]["nameLong"].getStr() + + result.add(%* { + "name": name, + "id": station["evaNumber"], + "dist": (int)round(calcDistMeters( + lat, + lon, + station["position"]["latitude"].getFloat(), + station["position"]["longitude"].getFloat(), + )), + }) + +proc getDepartures(evaId: string, limit: int = 10, duration: int = 60): JsonNode = + var response = risBoardsRequest("/public/departures/" & evaId, @[ + ("includeStationGroup", "true"), + ("maxViaStops", "0"), + ("sortBy", "TIME"), + ("duration", $duration) + ]) + + result = newJArray() + for departure in items(response["departures"]): + var direction: string + + if departure["transport"]["direction"]["text"].getStr() != "": + direction = departure["transport"]["direction"]["text"].getStr() + else: + direction = departure["transport"]["direction"]["stopPlaces"][0]["name"].getStr() + + result.add(%* { + "id": departure["transport"]["journeyID"], + "type": departure["transport"]["type"], + "category": departure["transport"]["category"], + "line": departure["transport"]["line"], + "direction": direction, + "platform": departure["platform"], + "time": toUnix(parse(departure["timeSchedule"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz").toTime()), + "delay": inSeconds(parse(departure["time"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz") - parse(departure["timeSchedule"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz")), + "canceled": departure["canceled"] + }) + + if (result.len == limit): + break + +proc getRoute(id: string): JsonNode = + var response = risJourneysRequest(id, @[ + ("separateCancelled", "true") + ]) + + result = %* [ + { + "id": response["events"][0]["stopPlace"]["evaNumber"], + "name": response["events"][0]["stopPlace"]["name"], + "platform": response["events"][0]["platform"], + "time": toUnix(parse(response["events"][0]["timeSchedule"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz").toTime()), + "delay": inSeconds(parse(response["events"][0]["time"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz") - parse(response["events"][0]["timeSchedule"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz")), + "canceled": false, + } + ] + var elemNum = 0 + for event in items(response["events"]): + if (event["type"].getStr() == "DEPARTURE") or (response["events"].len == elemNum): + result.add(%* { + "id": event["stopPlace"]["evaNumber"], + "name": event["stopPlace"]["name"], + "platform": event["platform"], + "time": toUnix(parse(event["timeSchedule"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz").toTime()), + "delay": inSeconds(parse(event["time"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz") - parse(event["timeSchedule"].getStr(), "yyyy-MM-dd'T'HH:mm:sszzz")), + "canceled": false, + }) + elemNum = elemNum+1 + +getCsrfToken() +var params = readData() + +write(stdout, "Content-Type: application/json; charset=UTF-8\n") +write(stdout, "Cache-Control: no-store, must-revalidate\n") +write(stdout, "Pragma: no-cache\n") +write(stdout, "Expires: 0\n") +write(stdout, "\n") + +if params["get"] == "stations": + write(stdout, $getStations( + parseFloat(params["lat"]), + parseFloat(params["lon"]), + parseInt(params.getOrDefault("limit", "5")), + parseInt(params.getOrDefault("radius", "750")), + )) + +if params["get"] == "departures": + write(stdout, $getDepartures( + params["evaId"], + parseInt(params["limit"]), + parseInt(params.getOrDefault("duration", "30")), + )) + +if params["get"] == "route": + write(stdout, $getRoute( + params["id"], + ))