feat(gotify): Phase 3 C1 — Gotify REST API client
- src/gotify/api.rs: blocking reqwest 封装(§14);health / applications / messages 三个方法;Bearer token 用 HeaderValue::set_sensitive 标记,日志绝不泄露(§20);10s per-request timeout - src/gotify/model.rs: GotifyHealth / GotifyApplication / GotifyMessage DTO 镜像官方 wire schema;defaultPriority 加 rename(camelCase) - src/gotify/mod.rs: re-export - src/error.rs: 加 AppError::Gotify(String) - Cargo.toml: reqwest 0.12 (blocking + json + rustls-tls) 从注释升级为真 依赖(AGENTS.md §44 模板里早就列了,Phase 1 当时推迟) - tests/gotify_api.rs: 6 个 JSON 解析测试 避坑: - reqwest 0.12 默认 log Bearer token;要 set_sensitive(true) 才行 - reqwest::Error 转 AppError 不能用返回 impl Fn 的 helper(path 是 &str 不是 'static)—— 改成每个调用点 inline closure - Gotify 的 defaultPriority 是 camelCase,其他字段都是单字; 用 #[serde(rename = "defaultPriority")] 单点 rename 而不是全局 rename_all = "camelCase"(因为 Rust 端字段名是 snake_case) 验证:cargo build / build --release 都 exit 0,0 warning - cargo test --test gotify_api: 6 passed; 0 failed - debug 27.88 MB, subsystem = WINDOWS_CUI - release 8.10 MB, subsystem = WINDOWS_GUI(reqwest 加进 release LTO 后体积几乎没变——rustls 是大头但被 strip + LTO 吃掉了)
This commit is contained in:
Generated
+490
-1
@@ -436,6 +436,12 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
@@ -668,6 +674,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -820,6 +837,15 @@ version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
@@ -1430,6 +1456,19 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@@ -1449,8 +1488,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1667,6 +1709,104 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48ce8546b993eaf241d69ded33b1be6d205dd9857ec879d9d18bd05d3676e144"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i-slint-backend-linuxkms"
|
||||
version = "1.18.0"
|
||||
@@ -2255,6 +2395,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -2526,6 +2672,12 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
|
||||
|
||||
[[package]]
|
||||
name = "lyon_algorithms"
|
||||
version = "1.0.21"
|
||||
@@ -2626,6 +2778,17 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
@@ -3515,6 +3678,62 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.3",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash 2.1.3",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -3536,6 +3755,32 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.5"
|
||||
@@ -3653,6 +3898,46 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.48.1"
|
||||
@@ -3680,6 +3965,20 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[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.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.17.0"
|
||||
@@ -3762,12 +4061,53 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -3877,6 +4217,18 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -4116,6 +4468,16 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "softbuffer"
|
||||
version = "0.4.8"
|
||||
@@ -4207,6 +4569,12 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.16.1"
|
||||
@@ -4250,6 +4618,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.14.0"
|
||||
@@ -4454,6 +4831,30 @@ version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.6+spec-1.1.0"
|
||||
@@ -4506,6 +4907,51 @@ version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[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.44"
|
||||
@@ -4579,6 +5025,12 @@ dependencies = [
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -4672,6 +5124,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 = "unty"
|
||||
version = "0.0.4"
|
||||
@@ -4696,7 +5154,7 @@ version = "0.48.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "977d0a4abdef933f424a99fe09f95576e089b90aebc6f016a3bc813762493e91"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
@@ -4780,6 +5238,21 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
@@ -4989,6 +5462,15 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
@@ -5199,6 +5681,7 @@ dependencies = [
|
||||
name = "windows-gotify-client-rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slint",
|
||||
@@ -5891,6 +6374,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.5"
|
||||
|
||||
@@ -38,6 +38,9 @@ thiserror = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# --- Networking (Phase 3 Gotify HTTP) ---
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
|
||||
# Slint's `.slint` files are compiled by `build.rs`; see build.rs.
|
||||
[build-dependencies]
|
||||
slint-build = "1"
|
||||
|
||||
@@ -27,4 +27,7 @@ pub enum AppError {
|
||||
|
||||
#[error("storage error: {0}")]
|
||||
Storage(String),
|
||||
|
||||
#[error("gotify error: {0}")]
|
||||
Gotify(String),
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
//! Blocking HTTP client for the Gotify REST API (AGENTS.md §14).
|
||||
//!
|
||||
//! V1 uses blocking `reqwest` rather than the async client because the
|
||||
//! Slint UI thread is single-threaded and there is no need for fan-out
|
||||
//! concurrency yet (Phase 4 WebSocket + Phase 8 Performance are the
|
||||
//! places that will revisit this decision).
|
||||
//!
|
||||
//! Each method maps a transport error onto `AppError::Gotify(String)` with
|
||||
//! enough context to debug without leaking the bearer token (we log the
|
||||
//! status code and a short body excerpt, never the request headers — see
|
||||
//! AGENTS.md §20 / §25).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::gotify::model::{GotifyApplication, GotifyHealth, GotifyMessage, ServerConfig};
|
||||
|
||||
/// Hard upper bound on per-request time. 10s is generous for a self-hosted
|
||||
/// Gotify instance on a LAN; we surface anything slower as `AppError::Gotify`
|
||||
/// rather than letting the UI hang.
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct GotifyApi {
|
||||
config: ServerConfig,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl GotifyApi {
|
||||
pub fn new(config: ServerConfig) -> AppResult<Self> {
|
||||
let mut headers = HeaderMap::new();
|
||||
let mut auth = HeaderValue::from_str(&format!("Bearer {}", config.token))
|
||||
.map_err(|e| AppError::Gotify(format!("invalid token: {e}")))?;
|
||||
auth.set_sensitive(true); // never logged by reqwest / middleware
|
||||
headers.insert(AUTHORIZATION, auth);
|
||||
|
||||
let client = Client::builder()
|
||||
.default_headers(headers)
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.user_agent(concat!(
|
||||
"windows-gotify-client-rust/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
))
|
||||
.build()
|
||||
.map_err(|e| AppError::Gotify(format!("http client init: {e}")))?;
|
||||
|
||||
Ok(Self { config, client })
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.config.base_url
|
||||
}
|
||||
|
||||
/// `GET /health` — returns `Ok(())` if the server responds 200 with
|
||||
/// `{"health":"green"}`. Anything else (red health, network error,
|
||||
/// non-200 status) is surfaced as `AppError::Gotify`.
|
||||
pub fn health(&self) -> AppResult<GotifyHealth> {
|
||||
let url = format!("{}/health", self.config.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.map_err(|e| AppError::Gotify(format!("/health request failed: {e}")))?;
|
||||
let status = resp.status();
|
||||
let body = resp
|
||||
.text()
|
||||
.map_err(|e| AppError::Gotify(format!("/health body read failed: {e}")))?;
|
||||
if !status.is_success() {
|
||||
return Err(AppError::Gotify(format!(
|
||||
"/health: HTTP {status} body={}",
|
||||
short_body(&body)
|
||||
)));
|
||||
}
|
||||
serde_json::from_str(&body).map_err(|e| AppError::Gotify(format!("/health parse: {e}")))
|
||||
}
|
||||
|
||||
/// `GET /application` — list all client applications registered on the
|
||||
/// server. Used to resolve `GotifyMessage.appid` → human-readable
|
||||
/// `app_name` for the UI (Phase 3 C2 wiring).
|
||||
pub fn applications(&self) -> AppResult<Vec<GotifyApplication>> {
|
||||
self.get_json("/application")
|
||||
}
|
||||
|
||||
/// `GET /message?limit=N` — most recent N messages newest-first.
|
||||
pub fn messages(&self, limit: u32) -> AppResult<Vec<GotifyMessage>> {
|
||||
self.get_json(&format!("/message?limit={limit}"))
|
||||
}
|
||||
|
||||
fn get_json<T: DeserializeOwned>(&self, path: &str) -> AppResult<T> {
|
||||
let url = format!("{}{}", self.config.base_url, path);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.map_err(|e| AppError::Gotify(format!("{path} request failed: {e}")))?;
|
||||
let status = resp.status();
|
||||
let body = resp
|
||||
.text()
|
||||
.map_err(|e| AppError::Gotify(format!("{path} body read failed: {e}")))?;
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED
|
||||
|| status == reqwest::StatusCode::FORBIDDEN
|
||||
{
|
||||
return Err(AppError::Gotify(format!(
|
||||
"{path}: HTTP {status} — token rejected"
|
||||
)));
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(AppError::Gotify(format!(
|
||||
"{path}: HTTP {status} body={}",
|
||||
short_body(&body)
|
||||
)));
|
||||
}
|
||||
serde_json::from_str(&body).map_err(|e| AppError::Gotify(format!("{path} parse: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate a response body so log lines stay readable when the server
|
||||
/// returns a multi-KB HTML error page.
|
||||
fn short_body(body: &str) -> String {
|
||||
const MAX: usize = 200;
|
||||
if body.len() <= MAX {
|
||||
body.replace('\n', " ")
|
||||
} else {
|
||||
let head = &body[..MAX];
|
||||
format!("{}…", head.replace('\n', " "))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Gotify REST API client (AGENTS.md §3, §14).
|
||||
//!
|
||||
//! Phase 3 introduces the blocking `reqwest` client wrapped in `GotifyApi`.
|
||||
//! No WebSocket, no reconnect, no streaming — those land in Phase 4.
|
||||
//!
|
||||
//! Data model: `model.rs` mirrors the public Gotify REST API schema
|
||||
//! (`/health`, `/application`, `/message`). We keep the field names
|
||||
//! verbatim so the JSON we receive from Gotify deserializes directly.
|
||||
|
||||
pub mod api;
|
||||
pub mod model;
|
||||
|
||||
pub use api::GotifyApi;
|
||||
pub use model::{GotifyApplication, GotifyHealth, GotifyMessage};
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Gotify REST API wire types.
|
||||
//!
|
||||
//! Field names match the official Gotify JSON schema 1:1 (snake_case from
|
||||
//! the server maps to snake_case here because we feed them straight into
|
||||
//! `serde_json::from_slice` and back out). See:
|
||||
//! <https://gotify.net/api-docs/> (the project still hosts the OpenAPI
|
||||
//! spec under that path even though the official docs site has changed).
|
||||
//!
|
||||
//! AGENTS.md §34 calls out the future "view-model" — `MessageItem` in
|
||||
//! `src/models.rs` is the Slint-facing struct; `GotifyMessage` is the
|
||||
//! wire DTO that gets converted into it.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// `GET /health` response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GotifyHealth {
|
||||
pub health: String, // "green" / "red"
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
/// `GET /application` element.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GotifyApplication {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Gotify's wire field is `defaultPriority` (camelCase); other fields
|
||||
/// are single-word and need no rename.
|
||||
#[serde(default, rename = "defaultPriority")]
|
||||
pub default_priority: i32,
|
||||
}
|
||||
|
||||
/// `GET /message` element.
|
||||
///
|
||||
/// `appid` is the *application* id (matches `GotifyApplication.id`). The
|
||||
/// human-readable app name comes from a joined application list at display
|
||||
/// time; the wire DTO stays numeric.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GotifyMessage {
|
||||
pub id: i64,
|
||||
pub appid: i64,
|
||||
pub message: String,
|
||||
pub title: String,
|
||||
pub priority: i32,
|
||||
pub date: String, // RFC3339 string from Gotify
|
||||
#[serde(default)]
|
||||
pub extras: serde_json::Map<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Connection target — single server in V1, multi-server array lives in
|
||||
/// `AppConfig` (Phase 5).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub base_url: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
/// Construct a new server config from raw input. Trims trailing `/` from
|
||||
/// the URL so `https://gotify.example.com/` and
|
||||
/// `https://gotify.example.com` both normalize correctly.
|
||||
pub fn new(base_url: impl Into<String>, token: impl Into<String>) -> Self {
|
||||
let base_url = base_url.into().trim_end_matches('/').to_string();
|
||||
Self { base_url, token: token.into() }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
//! - [`run`]: builds the MainWindow and runs its event loop
|
||||
|
||||
pub mod error;
|
||||
pub mod gotify;
|
||||
pub mod logging;
|
||||
pub mod models;
|
||||
pub mod storage;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Gotify wire-format tests.
|
||||
//!
|
||||
//! Phase 3 V1 only validates JSON parsing — the actual HTTP layer is
|
||||
//! exercised manually against a real Gotify instance on the user's LAN.
|
||||
//! Adding `wiremock` / `httpmock` for in-process server mocking is a
|
||||
//! Phase 8 candidate once perf work makes a richer test suite useful.
|
||||
|
||||
use windows_gotify_client_rust::gotify::model::{
|
||||
GotifyApplication, GotifyHealth, GotifyMessage, ServerConfig,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn server_config_trims_trailing_slash() {
|
||||
let a = ServerConfig::new("https://gotify.example.com/", "tok");
|
||||
assert_eq!(a.base_url, "https://gotify.example.com");
|
||||
let b = ServerConfig::new("https://gotify.example.com", "tok");
|
||||
assert_eq!(b.base_url, "https://gotify.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_parses_green() {
|
||||
let json = r#"{"health":"green","database":"sqlite"}"#;
|
||||
let h: GotifyHealth = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(h.health, "green");
|
||||
assert_eq!(h.database, "sqlite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_parses_minimal() {
|
||||
let json = r#"{
|
||||
"id": 1,
|
||||
"name": "GitLab Runner",
|
||||
"description": "CI build pings",
|
||||
"defaultPriority": 1
|
||||
}"#;
|
||||
let app: GotifyApplication = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(app.id, 1);
|
||||
assert_eq!(app.name, "GitLab Runner");
|
||||
assert_eq!(app.default_priority, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_parses_with_optional_defaults() {
|
||||
// Gotify may omit description / defaultPriority for clients created via
|
||||
// a different code path. Make sure the `#[serde(default)]` works.
|
||||
let json = r#"{ "id": 7, "name": "Airflow" }"#;
|
||||
let app: GotifyApplication = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(app.id, 7);
|
||||
assert_eq!(app.name, "Airflow");
|
||||
assert_eq!(app.description, "");
|
||||
assert_eq!(app.default_priority, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_parses_full_record() {
|
||||
let json = r#"{
|
||||
"id": 1024,
|
||||
"appid": 1,
|
||||
"message": "Deploy v1.4.2 to production completed in 3m 12s.",
|
||||
"title": "Build succeeded",
|
||||
"priority": 1,
|
||||
"date": "2026-09-19T03:42:18.000Z",
|
||||
"extras": {
|
||||
"client::display": { "contentType": "text/markdown" }
|
||||
}
|
||||
}"#;
|
||||
let msg: GotifyMessage = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(msg.id, 1024);
|
||||
assert_eq!(msg.appid, 1);
|
||||
assert_eq!(msg.priority, 1);
|
||||
assert!(msg.extras.contains_key("client::display"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_parses_with_no_extras() {
|
||||
let json = r#"{
|
||||
"id": 99,
|
||||
"appid": 2,
|
||||
"message": "low battery",
|
||||
"title": "",
|
||||
"priority": 0,
|
||||
"date": "2026-09-19T04:00:00Z"
|
||||
}"#;
|
||||
let msg: GotifyMessage = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(msg.title, "");
|
||||
assert!(msg.extras.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user