diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml new file mode 100644 index 0000000..9928b8d --- /dev/null +++ b/.gitea/workflows/main.yaml @@ -0,0 +1,67 @@ +name: Create and publish a Docker image + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + push: + branches: ['main'] + repository_dispatch: + +env: + REGISTRY: git.nickorlow.com + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.RUNNER_PKG_TOK }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Setup Python + uses: actions/setup-python@v4.0.0 + with: + python-version: 3.12 + + + - uses: paulhatch/semantic-version@v5.0.2 + id: vnum + with: + # The prefix to use to identify tags + tag_prefix: "" + major_pattern: "(MAJOR)" + minor_pattern: "(MINOR)" + version_format: "${major}.${minor}.${patch}-${increment}" + bump_each_commit: true + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vnum.outputs.version_tag }} + ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48a9305 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM rust:1.86.0 as build + +ENV PKG_CONFIG_ALLOW_CROSS=1 + +WORKDIR . +COPY ./api ./api +COPY ./libseptastic/ ./libseptastic/ + + +RUN cd api && cargo install --path . + +ENV RUST_LOG=info +ENV EXPOSE_PORT=80 + +EXPOSE 80 +ENTRYPOINT ["./api/septastic_api"] diff --git a/api/.envrc b/api/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/api/.envrc @@ -0,0 +1 @@ +use nix diff --git a/api/Cargo.lock b/api/Cargo.lock index 03b31d7..6efed8f 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -19,6 +19,44 @@ dependencies = [ "tracing", ] +[[package]] +name = "actix-cors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more 2.0.1", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-files" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags 2.9.1", + "bytes", + "derive_more 0.99.20", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + [[package]] name = "actix-http" version = "3.11.0" @@ -34,13 +72,13 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more", + "derive_more 2.0.1", "encoding_rs", "flate2", "foldhash", "futures-core", - "h2", - "http", + "h2 0.3.26", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -76,7 +114,7 @@ checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", - "http", + "http 0.2.12", "regex", "regex-lite", "serde", @@ -149,7 +187,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more", + "derive_more 2.0.1", "encoding_rs", "foldhash", "futures-core", @@ -230,6 +268,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.19" @@ -286,6 +339,48 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -477,6 +572,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -579,6 +683,30 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -600,6 +728,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cookie" version = "0.16.2" @@ -611,6 +745,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -689,6 +839,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -888,6 +1051,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1059,7 +1237,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -1149,6 +1346,46 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -1161,6 +1398,108 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.11", + "http 1.3.1", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1315,6 +1654,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1407,6 +1762,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libseptastic" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sqlx", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1429,6 +1793,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.8.0" @@ -1493,6 +1863,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1514,6 +1894,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1588,6 +1985,50 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.2.1" @@ -1632,6 +2073,24 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1879,6 +2338,60 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.11", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.8" @@ -1905,6 +2418,21 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.28" @@ -1932,6 +2460,52 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -1944,21 +2518,67 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "septastic_api" version = "0.1.0" dependencies = [ + "actix-cors", + "actix-files", "actix-web", "anyhow", + "askama", + "chrono", + "chrono-tz", "dotenv", "env_logger", + "libseptastic", "log", + "reqwest", + "serde", "serde_json", "sqlx", ] @@ -2054,6 +2674,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.10" @@ -2330,6 +2956,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2341,6 +2976,40 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand 2.3.0", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "2.0.12" @@ -2436,6 +3105,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.15" @@ -2449,6 +3138,51 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -2481,12 +3215,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -2520,6 +3266,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -2543,6 +3295,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "value-bag" version = "1.11.1" @@ -2567,6 +3325,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2701,6 +3468,76 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2849,6 +3686,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/api/Cargo.toml b/api/Cargo.toml index b2d8f80..43c9e85 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -11,3 +11,11 @@ env_logger = "0.11.8" log = "0.4.27" serde_json = "1.0.140" sqlx = { version = "0.8.6", features = [ "runtime-async-std", "postgres" ] } +libseptastic = { path = "../libseptastic/" } +askama = "0.14.0" +actix-files = "0.6.6" +serde = "1.0.219" +chrono = "0.4.41" +chrono-tz = "0.10.4" +actix-cors = "0.7.1" +reqwest = { version = "0.12.22", features = [ "json" ] } diff --git a/api/assets/style.css b/api/assets/style.css new file mode 100644 index 0000000..7c7eaae --- /dev/null +++ b/api/assets/style.css @@ -0,0 +1,134 @@ +* { + font-family: mono; +} + +th { + text-align: left; +} + +td { + text-align: left; +} + +table, th, td { + border: 1px solid black; +} + +body { + padding: 0 !important; + margin: 0 !important; +} + +.body { + background-color: #ffffff; + margin: 10px auto; + max-width: 750px; + width: 95%; +} + +a { + text-decoration: none; + color: #114488; +} + +p, h1, h2, h3, h4, h5, h6 { + margin: 0; +} + +img { + max-width: 100%; +} + +.flag-img { + height: 30px; +} + +.nav-link { + white-space: nowrap; +} + +.metro-container { + font-size: 1.5em; + padding: 0.3em; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + width: max-content; + height: max-content; + aspect-ratio: 1 / 1; + line-height: 1; +} + +.rr-container { + background-color: #4c748c; + color: #ffffff; + font-size: 1.5em; + padding: 0.3em; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + width: max-content; + height: max-content; + line-height: 1; +} + +.bg-B1, .bg-B2, .bg-B3 { + background-color: #FF661C; + color: #ffffff; +} + +.bg-L1 { + background-color: #009BDE; + color: #ffffff; +} + +.bg-M1 { + background-color: #552B9D; + color: #ffffff; +} + +.bg-D1, .bg-D2 { + background-color: #EA4379; + color: #ffffff; +} + +.bg-G1 { + background-color: #FFD500; + color: #000000; +} + +.bg-T1, .bg-T2, .bg-T3, .bg-T4, .bg-T5 { + background-color: #6EA516; + color: #ffffff; +} + +.bus-container { + display: inline-block; + padding: 0.2em 0.5em; /* scales with font size */ + font-size: 1em; /* or inherit */ + font-weight: bold; + border-radius: 9999px; /* full pill shape */ + border: 2px solid #000000; + background-color: #ffffff; + color: #000000; + width: max-content; + line-height: 1; +} +.tscroll { + width: 100%; + overflow: scroll; + margin-bottom: 10px; +} + +.tscroll table td:first-child, .tscroll table th:first-child { + position: sticky; + left: 0; + background-color: #ddd; + box-shadow: inset 0 0.5px 0 #000000,inset 0 -0.5px 0 #000000,inset 1px 0 0 #000000,inset -1px 0 0 #000000; + border-width:0; +} + +.tscroll td, .tscroll th { +} diff --git a/api/assets/test.html b/api/assets/test.html new file mode 100644 index 0000000..618a6c3 --- /dev/null +++ b/api/assets/test.html @@ -0,0 +1,65 @@ + + + + + Canvas Curve Full Width + + + + + + + + diff --git a/api/shell.nix b/api/shell.nix new file mode 100644 index 0000000..5e00bc7 --- /dev/null +++ b/api/shell.nix @@ -0,0 +1,8 @@ +with import {}; +stdenv.mkDerivation { + name = "env"; + nativeBuildInputs = [ pkg-config ]; + buildInputs = [ + cryptsetup + ]; +} diff --git a/api/src/database.rs b/api/src/database.rs new file mode 100644 index 0000000..86e72a2 --- /dev/null +++ b/api/src/database.rs @@ -0,0 +1,298 @@ +use std::{collections::HashMap, hash::Hash}; + +use actix_web::Route; +use libseptastic::{direction::CardinalDirection, route::RouteType}; +use serde::{Deserialize, Serialize}; +use sqlx::{Postgres, Transaction}; + +pub async fn get_route_by_id( + id: String, + transaction: &mut Transaction<'_, Postgres>, +) -> ::anyhow::Result { + + let row = sqlx::query!( + r#"SELECT + id, + name, + short_name, + color_hex, + route_type as "route_type: libseptastic::route::RouteType" + FROM + septa_routes + WHERE + id = $1 + ;"#, + id + ) + .fetch_one(&mut **transaction) + .await?; + + return Ok(libseptastic::route::Route { + name: row.name, + short_name: row.short_name, + color_hex: row.color_hex, + route_type: row.route_type, + id: row.id, + }); +} + +pub async fn get_direction_by_route_id( + id: String, + transaction: &mut Transaction<'_, Postgres>, +) -> ::anyhow::Result> { + + let rows = sqlx::query!( + r#"SELECT + route_id, + direction_id, + direction as "direction: libseptastic::direction::CardinalDirection", + direction_destination + FROM + septa_directions + WHERE + route_id = $1 + ;"#, + id + ) + .fetch_all(&mut **transaction) + .await?; + + let mut res = Vec::new(); + + for row in rows { + res.push(libseptastic::direction::Direction{ + route_id: row.route_id, + direction_id: row.direction_id, + direction: row.direction, + direction_destination: row.direction_destination + }); + } + + return Ok(res); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StopSchedule { + pub route_id: String, + pub stop_name: String, + pub trip_id: String, + pub service_id: String, + pub direction_id: i64, + pub arrival_time: i64, + pub stop_id: i64, + pub stop_sequence: i64 +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Trip { + pub route_id: String, + pub trip_id: String, + pub direction_id: i64, + pub schedule: Vec +} + +pub async fn get_schedule_by_route_id( + id: String, + transaction: &mut Transaction<'_, Postgres>, +) -> ::anyhow::Result> { + + let rows = sqlx::query!( + r#"SELECT + septa_stop_schedules.route_id, + septa_stops.name as stop_name, + trip_id, + service_id, + septa_stop_schedules.direction_id, + septa_directions.direction as "direction: libseptastic::direction::CardinalDirection", + arrival_time, + stop_id, + stop_sequence + FROM + septa_stop_schedules + INNER JOIN septa_directions + ON + septa_directions.direction_id = septa_stop_schedules.direction_id + AND + septa_directions.route_id = septa_stop_schedules.route_id + INNER JOIN septa_stops + ON septa_stops.id = septa_stop_schedules.stop_id + WHERE + septa_stop_schedules.route_id = $1 + AND + service_id IN (SELECT service_id FROM septa_schedule_days WHERE date = '20250707') + ;"#, + id + ) + .fetch_all(&mut **transaction) + .await?; + + let mut sched_groups: HashMap> = HashMap::new(); + for row in rows { + let mut arr = match sched_groups.get_mut(&row.trip_id) { + Some(x) => x, + None => { + sched_groups.insert(row.trip_id.clone(), Vec::new()); + sched_groups.get_mut(&row.trip_id).unwrap() + } + }; + + arr.push(StopSchedule { + route_id: row.route_id, + stop_name: row.stop_name, + trip_id: row.trip_id, + service_id: row.service_id, + direction_id: row.direction_id, + arrival_time: row.arrival_time, + stop_id: row.stop_id, + stop_sequence: row.stop_sequence + }); + } + let mut res = Vec::new(); + + for group in sched_groups { + res.push(Trip{ + trip_id: group.0, + route_id: group.1[0].route_id.clone(), + schedule: group.1.clone(), + direction_id: group.1[0].direction_id.clone() + }); + } + + return Ok(res); +} + +#[derive(Serialize,Deserialize,Clone)] +pub struct NTALive { + delay: i64, + cancelled: bool, + next_stop: Option +} + +#[derive(Serialize,Deserialize)] +pub struct LiveData { + route_id: String, + service_id: String, + trip_id: String, + trip_headsign: String, + direction_id: i64, + block_id: String, + start_time: String, + end_time: String, + delay: i64, + status: String, + lat: Option, + lon: Option, + heading: Option, + next_stop_id: Option, + next_stop_name: Option, + next_stop_sequence: Option, + seat_availability: String, + vehicle_id: String, + timestamp: i64 +} + +#[derive(Serialize,Deserialize)] +pub struct NTAEntry { + route_id: String, + route_type: RouteType, + route_name: String, + color_hex: String, + trip_id: String, + arrival_time: i64, + direction: CardinalDirection, + direction_destination: String, + live: Option +} + +#[derive(Serialize,Deserialize)] +pub struct NTAResult { + station_name: String, + arrivals: Vec +} + +pub async fn get_nta_by_stop_id( + ids: Vec, + start_time: chrono::DateTime, + end_time: chrono::DateTime, + transaction: &mut Transaction<'_, Postgres>, +) -> ::anyhow::Result { + let local_start = start_time.with_timezone(&chrono_tz::America::New_York); + let local_end = end_time.with_timezone(&chrono_tz::America::New_York); + let local_midnight = chrono::Utc::now().with_timezone(&chrono_tz::America::New_York).date().and_hms(0,0,0); + let start_secs = local_start.signed_duration_since(local_midnight).num_seconds(); + let end_secs = local_end.signed_duration_since(local_midnight).num_seconds(); + + let name_row = sqlx::query!("SELECT name FROM septa_stops WHERE id = $1", ids[0]).fetch_one(&mut **transaction).await?; + + let stop_name = name_row.name; + + let rows: Vec<(String, RouteType, String, String, i64, CardinalDirection, String, String,)> = sqlx::query_as( + r#"SELECT + septa_stop_schedules.route_id, + route_type as "route_type: libseptastic::route::RouteType", + septa_routes.color_hex, + trip_id, + arrival_time, + septa_directions.direction as "direction: libseptastic::direction::CardinalDirection", + septa_directions.direction_destination, + septa_routes.name + FROM + septa_stop_schedules + INNER JOIN septa_directions + ON + septa_directions.direction_id = septa_stop_schedules.direction_id + AND + septa_directions.route_id = septa_stop_schedules.route_id + INNER JOIN septa_stops + ON septa_stops.id = septa_stop_schedules.stop_id + INNER JOIN septa_routes + ON septa_routes.id = septa_stop_schedules.route_id + WHERE + (septa_stops.id = $1 OR septa_stops.id = $2) + AND + service_id IN (SELECT service_id FROM septa_schedule_days WHERE date = '20250707') + AND + septa_stop_schedules.arrival_time > $3 + AND + septa_stop_schedules.arrival_time < $4 + ORDER BY arrival_time + ;"#) + .bind(&ids[0]) + .bind(&ids.get(1).unwrap_or(&0)) + .bind(&start_secs) + .bind(&end_secs) + .fetch_all(&mut **transaction) + .await?; + + let mut ntas: Vec = Vec::new(); + let mut live_map: HashMap = HashMap::new(); + + let lives: Vec = reqwest::get("https://www3.septa.org/api/v2/trips/?route_id=AIR,CHW,LAN,NOR,TRE,WIL,WAR,MED,PAO,FOX,WTR,CYN").await?.json().await?; + + for live in lives { + live_map.insert(live.route_id, NTALive { delay: live.delay, cancelled: live.status == "CANCELLED", next_stop: live.next_stop_name }); + } + + for row in rows { + ntas.push(NTAEntry { + route_id: row.0.clone(), + route_type: row.1, + color_hex: row.2, + trip_id: row.3, + arrival_time: row.4, + direction: row.5, + direction_destination: row.6, + route_name: row.7, + live: match live_map.get(&row.0) { + Some(x) => Some(x.clone()), + None => None + } + }); + } + + return Ok(NTAResult{ + station_name: stop_name, + arrivals: ntas + }); +} diff --git a/api/src/main.rs b/api/src/main.rs index fa69995..086e82f 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1,12 +1,229 @@ -use actix_web::{get, App, HttpResponse, HttpServer, Responder}; +use actix_web::{get, web::{self, Data}, App, HttpResponse, HttpServer, Responder}; +use chrono::TimeDelta; +use database::{get_direction_by_route_id, get_nta_by_stop_id, get_schedule_by_route_id}; use env_logger::Env; +use libseptastic::{direction::Direction}; +use database::{Trip, StopSchedule}; use log::*; use dotenv::dotenv; -use serde_json::json; +use std::{cmp::Ordering, collections::BTreeMap, sync::Arc}; +use askama::Template; +use serde::{Serialize}; + +mod database; + +struct AppState { + database: ::sqlx::postgres::PgPool +} + + +async fn get_route_by_id(id: String, state: Data>) -> ::anyhow::Result { + Ok(database::get_route_by_id(id, &mut state.database.begin().await?).await?) +} + + +#[derive(Debug, Serialize)] +pub struct TimetableStopRow { + pub stop_id: i64, + pub stop_name: String, + pub stop_sequence: i64, + pub times: Vec>, // one per trip, None if trip doesn't stop +} + +#[derive(Debug, Serialize)] +pub struct TimetableDirection { + pub direction: Direction, + pub trip_ids: Vec, // column headers + pub rows: Vec, // one per unique stop +} + +pub fn build_timetables( + directions: &[Direction], + trips: &[Trip], +) -> Vec { + let mut results = Vec::new(); + + for direction in directions { + let mut direction_trips: Vec<&Trip> = trips + .iter() + .filter(|trip| trip.direction_id == direction.direction_id) + .collect(); +direction_trips.sort_by_key(|trip| { + trip.schedule + .iter() + .filter_map(|s| Some(s.arrival_time)) + .min() + .unwrap_or(i64::MAX) +}); + + let trip_ids: Vec = direction_trips + .iter() + .map(|t| t.trip_id.clone()) + .collect(); + + // Map of stop_id -> (stop_sequence, Vec>) + let mut stop_map: BTreeMap>)> = BTreeMap::new(); + + for (trip_index, trip) in direction_trips.iter().enumerate() { + for stop in &trip.schedule { + let entry = stop_map + .entry(stop.stop_id) + .or_insert((stop.stop_sequence, stop.stop_name.clone(), vec![None; direction_trips.len()])); + + // If this stop_id appears in multiple trips with different sequences, keep the lowest + entry.0 = entry.0.max(stop.stop_sequence); + entry.1 = stop.stop_name.clone(); + entry.2[trip_index] = Some(stop.arrival_time); + } + } + + let mut rows: Vec = stop_map + .into_iter() + .map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow { + stop_id, + stop_sequence, + stop_name, + times, + }) + .collect(); + + rows.sort_by(| a, b| { + if a.stop_sequence < b.stop_sequence { + Ordering::Less + } else { + Ordering::Greater + } + }); + + + results.push(TimetableDirection { + direction: direction.clone(), + trip_ids, + rows, + }); + } + + results +} + +mod filters { + pub fn format_time( + seconds: &i64, + _: &dyn askama::Values, + ) -> askama::Result { + let total_minutes = seconds / 60; + let (hours, ampm) = { + let hrs = total_minutes / 60; + if hrs > 12 { + (hrs - 12, "PM") + } else { + (hrs, "AM") + } + }; + let minutes = total_minutes % 60; + Ok(format!("{}:{:02} {}", hours, minutes, ampm)) + } +} + +#[derive(askama::Template)] +#[template(path = "layout.html")] +struct ContentTemplate { + content: T, + page_title: Option, + page_desc: Option, +} + +#[derive(askama::Template)] +#[template(path = "route.html")] +struct RouteTemplate { + route: libseptastic::route::Route, + directions: Vec, + timetables: Vec +} + +#[derive(askama::Template)] +#[template(path = "routes.html")] +struct RoutesTemplate { +} + +#[derive(askama::Template)] +#[template(path = "index.html")] +struct IndexTemplate { +} + +#[get("/routes")] +async fn get_routes() -> impl Responder { + + HttpResponse::Ok().body(ContentTemplate { + page_title: None, + page_desc: None, + content: RoutesTemplate {} + }.render().unwrap()) +} #[get("/")] -async fn hello() -> impl Responder { - HttpResponse::Ok().json("{}") +async fn get_index() -> impl Responder { + + HttpResponse::Ok().body(ContentTemplate { + page_title: None, + page_desc: None, + content: IndexTemplate {} + }.render().unwrap()) +} + +#[get("/route/{route_id}")] +async fn get_route(state: Data>, path: web::Path<(String)>) -> impl Responder { + let route_id = path.into_inner(); + let route_r = get_route_by_id(route_id.clone(), state.clone()).await; + let directions = get_direction_by_route_id(route_id.clone(), &mut state.database.begin().await.unwrap()).await.unwrap(); + let trips = get_schedule_by_route_id(route_id, &mut state.database.begin().await.unwrap()).await.unwrap(); + if let Ok(route) = route_r { + HttpResponse::Ok().body(ContentTemplate { + page_title: None, + page_desc: None, + content: RouteTemplate { + route, + directions: directions.clone(), + timetables: build_timetables(directions.as_slice(), trips.as_slice()) + } + }.render().unwrap()) + } else { + HttpResponse::InternalServerError().body("Error") + } +} + +#[get("/api/route/{route_id}")] +async fn api_get_route(state: Data>, path: web::Path<(String)>) -> impl Responder { + let route_id = path.into_inner(); + let route_r = get_route_by_id(route_id, state).await; + if let Ok(route) = route_r { + HttpResponse::Ok().json(route) + } else { + HttpResponse::InternalServerError().body("Error") + } +} + +#[get("/api/route/{route_id}/schedule")] +async fn api_get_schedule(state: Data>, path: web::Path<(String)>) -> impl Responder { + let route_id = path.into_inner(); + let route_r = get_schedule_by_route_id(route_id, &mut state.database.begin().await.unwrap()).await; + if let Ok(route) = route_r { + HttpResponse::Ok().json(route) + } else { + HttpResponse::InternalServerError().body("Error") + } +} + +#[get("/api/stop/{stop_id}/nta")] +async fn api_get_nta(state: Data>, path: web::Path<(String)>) -> impl Responder { + let route_id = path.into_inner().split(',') .map(|s| s.parse::()) + .collect::, _>>().unwrap(); + let route_r = get_nta_by_stop_id(route_id, chrono::Utc::now(), chrono::Utc::now() + TimeDelta::minutes(30), &mut state.database.begin().await.unwrap()).await; + if let Ok(route) = route_r { + HttpResponse::Ok().json(route) + } else { + HttpResponse::InternalServerError().body(format!("Error {:?}", route_r.err())) + } } #[actix_web::main] @@ -27,12 +244,23 @@ async fn main() -> ::anyhow::Result<()> { .connect(&connection_string) .await?; - let mut transaction = pool.begin().await?; - HttpServer::new(|| { + let state = Arc::new(AppState { + database: pool + }); + + HttpServer::new(move || { App::new() - .service(hello) + .wrap(actix_cors::Cors::permissive()) + .app_data(Data::new(state.clone())) + .service(api_get_route) + .service(api_get_schedule) + .service(api_get_nta) + .service(get_route) + .service(get_routes) + .service(get_index) + .service(actix_files::Files::new("/assets", "./assets")) }) - .bind(("127.0.0.1", 8080))? + .bind(("0.0.0.0", 8080))? .run() .await?; diff --git a/api/templates/index.html b/api/templates/index.html new file mode 100644 index 0000000..6bf0564 --- /dev/null +++ b/api/templates/index.html @@ -0,0 +1,13 @@ +

SEPTASTIC!

+

A fantastic way to ride SEPTA

+ +

+ SEPTASTIC is a website and (a soon to be) mobile app. Its purpose is to provide + information about how to ride SEPTA (and connecting transit authorities) in a + quick and information-rich manner. +

+ +

+ Currently, all this website has is timetables for every + SEPTA route. More to come soon! +

diff --git a/api/templates/layout.html b/api/templates/layout.html new file mode 100644 index 0000000..f4ffa25 --- /dev/null +++ b/api/templates/layout.html @@ -0,0 +1,58 @@ + + + + + {% if let Some(title) = page_title %} + {{ title }} + {% else %} + SEPTASTIC + {% endif %} + + {% if let Some(desc) = page_desc %} + + {% else %} + + {% endif %} + + + + + + + +
+ +
+ + {{ content|safe }} + + +
+ + diff --git a/api/templates/route.html b/api/templates/route.html new file mode 100644 index 0000000..589c64b --- /dev/null +++ b/api/templates/route.html @@ -0,0 +1,111 @@ +{%- import "route_symbol.html" as scope -%} + + + + +
+ This website is not run by SEPTA. As such, schedules may not be + completely accurate. +
+ +
+ {% call scope::route_symbol(route) %} +

{{ route.name }}

+
+ +{% for timetable in timetables %} +

{{ timetable.direction.direction | capitalize }} to {{ timetable.direction.direction_destination }}

+
+ + + + + {% for trip_id in timetable.trip_ids %} + + {% endfor %} + + + + {% for row in timetable.rows %} + + + {% for time in row.times %} + + {% endfor %} + + {% endfor %} + +
Stop{{ trip_id }}
{{ row.stop_name }} + {% if let Some(t) = time %} + {{ t | format_time }} + {% else %} + -- + {% endif %} +
+
+{% endfor %} diff --git a/api/templates/route_symbol.html b/api/templates/route_symbol.html new file mode 100644 index 0000000..e5551f2 --- /dev/null +++ b/api/templates/route_symbol.html @@ -0,0 +1,19 @@ +{% macro route_symbol(route) %} + {% match route.route_type %} + {% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %} +
+ {{ route.id }} +
+ {% endwhen %} + {% when libseptastic::route::RouteType::RegionalRail %} +
+ {{ route.id }} +
+ {% endwhen %} + {% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %} +
+ {{ route.id }} +
+ {% endwhen %} + {% endmatch %} +{% endmacro %} diff --git a/api/templates/routes.html b/api/templates/routes.html new file mode 100644 index 0000000..978f54b --- /dev/null +++ b/api/templates/routes.html @@ -0,0 +1,83 @@ +

Routes

+ +

Click on a route to see details and a schedule. Schedules in prevailing local time.

+ +
+

Regional Rail

+

For infrequent rail service to suburban locations

+ + + + + + + + + + + + + + + + +
+ +
+

Metro

+

For frequent rail service within Philadelphia and suburban locations

+

[ Subway/Elevated ]

+ + + + + + + +

[ Urban Trolley ]

+ + + + + + +

[ Suburban Trolley ]

+ + + +
+ +
+

Bus

+

For service of varying frequency within SEPTA's entire service area

+

[ Subway/Elevated ]

+ + + + +

[ Urban Trolley ]

+ + + + + + +

[ Suburban Trolley ]

+ + + +
+ + diff --git a/data_loader b/data_loader index d577568..f7e9e79 160000 --- a/data_loader +++ b/data_loader @@ -1 +1 @@ -Subproject commit d577568fec8b1b7356cd0bb0520a20283318096a +Subproject commit f7e9e7903b55ed5353b0ea3946dcb4b131557468 diff --git a/libseptastic/Cargo.lock b/libseptastic/Cargo.lock index d8ba565..1260fb2 100644 --- a/libseptastic/Cargo.lock +++ b/libseptastic/Cargo.lock @@ -538,6 +538,8 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" name = "libseptastic" version = "0.1.0" dependencies = [ + "serde", + "serde_json", "sqlx", ] diff --git a/libseptastic/Cargo.toml b/libseptastic/Cargo.toml index fa289d4..3f44792 100644 --- a/libseptastic/Cargo.toml +++ b/libseptastic/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] +serde = "1.0.219" +serde_json = "1.0.140" sqlx = "0.8.6" diff --git a/libseptastic/src/direction.rs b/libseptastic/src/direction.rs new file mode 100644 index 0000000..beb68b6 --- /dev/null +++ b/libseptastic/src/direction.rs @@ -0,0 +1,37 @@ + +use serde::{Deserialize, Serialize}; + +#[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone)] +#[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")] +pub enum CardinalDirection { + Northbound, + Southbound, + Eastbound, + Westbound, + Inbound, + Outbound, + Loop +} + +#[derive(::sqlx::Decode, Serialize, Deserialize, Debug, Clone)] +pub struct Direction { + pub route_id: String, + pub direction_id: i64, + pub direction: CardinalDirection, + pub direction_destination: String +} + +impl std::fmt::Display for CardinalDirection { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let output = match self { + CardinalDirection::Northbound => "Northbound", + CardinalDirection::Southbound => "Southbound", + CardinalDirection::Eastbound => "Eastbound", + CardinalDirection::Westbound => "Westbound", + CardinalDirection::Inbound => "Inbound", + CardinalDirection::Outbound => "Outbound", + CardinalDirection::Loop => "Loop" + }; + std::write!(f, "{}", output) + } +} diff --git a/libseptastic/src/lib.rs b/libseptastic/src/lib.rs index bcac679..af16fa4 100644 --- a/libseptastic/src/lib.rs +++ b/libseptastic/src/lib.rs @@ -3,3 +3,5 @@ pub mod stop; pub mod route_stop; pub mod stop_schedule; pub mod schedule_day; +pub mod direction; +pub mod ridership; diff --git a/libseptastic/src/ridership.rs b/libseptastic/src/ridership.rs new file mode 100644 index 0000000..84bc2cd --- /dev/null +++ b/libseptastic/src/ridership.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::direction::CardinalDirection; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ridership { + pub route_id: String, + pub stop_id: i64, + pub direction: CardinalDirection, + pub exp_ons: i64, + pub exp_offs: i64, + pub ons: i64, + pub offs: i64, + pub year: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LineRidership { + pub route_id: String, + pub unlinked_trips: i64 +} diff --git a/libseptastic/src/route.rs b/libseptastic/src/route.rs index f1ff26d..2a14c02 100644 --- a/libseptastic/src/route.rs +++ b/libseptastic/src/route.rs @@ -1,4 +1,6 @@ -#[derive(sqlx::Type, PartialEq, Debug, Clone)] +use serde::{Deserialize, Serialize}; + +#[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone)] #[sqlx(type_name = "septa_route_type", rename_all = "snake_case")] pub enum RouteType { Trolley, @@ -8,34 +10,11 @@ pub enum RouteType { TracklessTrolley } -#[derive(sqlx::Type, PartialEq, Debug, Clone)] -#[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")] -pub enum CardinalDirection { - Northbound, - Southbound, - Eastbound, - Westbound // (and down) -} - -#[derive(Debug, Clone)] -pub struct Directional { - pub direction: CardinalDirection, - pub direction_destination: String -} - - -#[derive(Debug, Clone)] -pub struct RouteDirectional { - pub primary: Directional, // 0 - pub secondary: Directional, // 1 -} - -#[derive(Debug, Clone)] +#[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)] pub struct Route { pub name: String, pub short_name: String, pub color_hex: String, pub route_type: RouteType, - pub id: String, - pub directional: RouteDirectional + pub id: String } diff --git a/libseptastic/src/route_stop.rs b/libseptastic/src/route_stop.rs index 77d941d..8d9d4b4 100644 --- a/libseptastic/src/route_stop.rs +++ b/libseptastic/src/route_stop.rs @@ -1,9 +1,7 @@ -use super::route::CardinalDirection; - #[derive(Debug, Clone)] pub struct RouteStop { pub route_id: String, pub stop_id: i64, - pub direction: CardinalDirection, + pub direction_id: i64, pub stop_sequence: i64 } diff --git a/libseptastic/src/stop_schedule.rs b/libseptastic/src/stop_schedule.rs index b09c38f..a66f19e 100644 --- a/libseptastic/src/stop_schedule.rs +++ b/libseptastic/src/stop_schedule.rs @@ -1,12 +1,20 @@ -use super::route::CardinalDirection; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct StopSchedule { pub route_id: String, pub trip_id: String, pub service_id: String, - pub direction: CardinalDirection, + pub direction_id: i64, pub arrival_time: i64, pub stop_id: i64, pub stop_sequence: i64 } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Trip { + pub route_id: String, + pub trip_id: String, + pub direction_id: i64, + pub schedule: Vec +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..005c9aa --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +with import {}; +stdenv.mkDerivation { + name = "septastic_env"; + nativeBuildInputs = [ pkg-config ]; + buildInputs = [ + cryptsetup + ]; +}