Personal ATProto tools.

initialize

+6989
+100
.clippy.toml
··· 1 + allow-unwrap-in-tests = true 2 + # https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api 3 + avoid-breaking-exported-api = false 4 + excessive-nesting-threshold = 7 5 + max-fn-params-bools = 0 6 + # https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file 7 + max-include-file-size = 1000000 8 + # https://rust-lang.github.io/rust-clippy/master/index.html#/large_stack_frames 9 + stack-size-threshold = 65536 10 + too-many-lines-threshold = 64 11 + array-size-threshold = 32 12 + future-size-threshold = 8192 13 + type-complexity-threshold = 200 14 + cognitive-complexity-threshold = 7 15 + missing-docs-in-crate-items = true 16 + check-private-items = true 17 + too-many-arguments-threshold = 5 18 + 19 + # ----------------------------------------------------------------------------- 20 + 21 + # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros 22 + disallowed-macros = [ 23 + 'dbg', 24 + 'std::eprint', 25 + 'std::eprintln', 26 + 'std::print', 27 + # 'std::println', 28 + # 'std::unimplemented', 29 + ] 30 + 31 + # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods 32 + disallowed-methods = [ 33 + { path = "log::error", reason = "use tracing::error" }, 34 + { path = "log::warn", reason = "use tracing::warn" }, 35 + { path = "log::info", reason = "use tracing::info" }, 36 + { path = "log::debug", reason = "use tracing::debug" }, 37 + { path = "log::trace", reason = "use tracing::trace" }, 38 + { path = "sha1::Digest::new", reason = "SHA1 is cryptographically broken" }, 39 + { path = "std::env::temp_dir", reason = "Use the tempdir crate instead" }, 40 + { path = "std::panic::catch_unwind", reason = "We compile with `panic = 'abort'`" }, 41 + { path = "std::thread::spawn", reason = "Use `std::thread::Builder` and name the thread" }, 42 + 43 + # There are many things that aren't allowed on wasm, 44 + # but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406) 45 + { path = "crossbeam::channel::Receiver::into_iter", reason = "Cannot block on Web" }, 46 + { path = "crossbeam::channel::Receiver::iter", reason = "Cannot block on Web" }, 47 + { path = "crossbeam::channel::Receiver::recv_timeout", reason = "Cannot block on Web" }, 48 + { path = "crossbeam::channel::Receiver::recv", reason = "Cannot block on Web" }, 49 + { path = "poll_promise::Promise::block_and_take", reason = "Cannot block on Web" }, 50 + { path = "poll_promise::Promise::block_until_ready_mut", reason = "Cannot block on Web" }, 51 + { path = "poll_promise::Promise::block_until_ready", reason = "Cannot block on Web" }, 52 + { path = "pollster::block_on", reason = "Cannot block on Web" }, 53 + { path = "rayon::spawn", reason = "Cannot spawn threads on wasm" }, 54 + { path = "std::sync::mpsc::Receiver::into_iter", reason = "Cannot block on Web" }, 55 + { path = "std::sync::mpsc::Receiver::iter", reason = "Cannot block on Web" }, 56 + { path = "std::sync::mpsc::Receiver::recv_timeout", reason = "Cannot block on Web" }, 57 + { path = "std::sync::mpsc::Receiver::recv", reason = "Cannot block on Web" }, 58 + { path = "std::thread::spawn", reason = "Cannot spawn threads on wasm" }, 59 + { path = "std::time::Duration::elapsed", reason = "use `web-time` crate instead for wasm/web compatibility" }, 60 + { path = "std::time::Instant::now", reason = "use `web-time` crate instead for wasm/web compatibility" }, 61 + { path = "std::time::SystemTime::now", reason = "use `web-time` or `time` crates instead for wasm/web compatibility" }, 62 + ] 63 + 64 + # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names 65 + disallowed-names = [] 66 + 67 + # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types 68 + disallowed-types = [ 69 + { path = "ring::digest::SHA1_FOR_LEGACY_USE_ONLY", reason = "SHA1 is cryptographically broken" }, 70 + { path = "std::sync::Condvar", reason = "Use parking_lot instead" }, 71 + { path = "std::sync::Mutex", reason = "Use parking_lot instead" }, 72 + { path = "std::sync::RwLock", reason = "Use parking_lot instead" }, 73 + 74 + # There are many things that aren't allowed on wasm, 75 + { path = "instant::SystemTime", reason = "Known bugs. Use web-time." }, 76 + { path = "std::thread::Builder", reason = "Cannot spawn threads on wasm" }, 77 + # { path = "std::path::PathBuf", reason = "Can't read/write files on web" }, 78 + ] 79 + 80 + # Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown 81 + doc-valid-idents = [ 82 + "..", 83 + "GitHub", 84 + "GLB", 85 + "GLTF", 86 + "iOS", 87 + "macOS", 88 + "MessagePack", 89 + "MiMalloc", 90 + "NaN", 91 + "OBJ", 92 + "OpenGL", 93 + "PyPI", 94 + "sRGB", 95 + "sRGBA", 96 + "WebGL", 97 + "WebGPU", 98 + "WebSocket", 99 + "WebSockets", 100 + ]
+6
.env.example
··· 1 + ACCESS_JWT= 2 + REFRESH_JWT= 3 + SELF_DID= 4 + NEGATION_POST_URI= 5 + ADDITION_POST_URI= 6 + PRIVATE_KEY_HEX=
+1
.envrc
··· 1 + use flake
+4
.gitignore
··· 1 + /.direnv 2 + /target 3 + .env 4 + prod.db
+3651
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.24.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.0" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 + 20 + [[package]] 21 + name = "aho-corasick" 22 + version = "1.1.3" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 + dependencies = [ 26 + "memchr", 27 + ] 28 + 29 + [[package]] 30 + name = "alloc-no-stdlib" 31 + version = "2.0.4" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 34 + 35 + [[package]] 36 + name = "alloc-stdlib" 37 + version = "0.2.2" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 40 + dependencies = [ 41 + "alloc-no-stdlib", 42 + ] 43 + 44 + [[package]] 45 + name = "allocator-api2" 46 + version = "0.2.21" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 49 + 50 + [[package]] 51 + name = "android-tzdata" 52 + version = "0.1.1" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 55 + 56 + [[package]] 57 + name = "android_system_properties" 58 + version = "0.1.5" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 61 + dependencies = [ 62 + "libc", 63 + ] 64 + 65 + [[package]] 66 + name = "arrayvec" 67 + version = "0.7.6" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 70 + 71 + [[package]] 72 + name = "async-compression" 73 + version = "0.4.18" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" 76 + dependencies = [ 77 + "brotli", 78 + "flate2", 79 + "futures-core", 80 + "memchr", 81 + "pin-project-lite", 82 + "tokio", 83 + "zstd", 84 + "zstd-safe", 85 + ] 86 + 87 + [[package]] 88 + name = "async-trait" 89 + version = "0.1.85" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" 92 + dependencies = [ 93 + "proc-macro2", 94 + "quote", 95 + "syn", 96 + ] 97 + 98 + [[package]] 99 + name = "atoi" 100 + version = "2.0.0" 101 + source = "registry+https://github.com/rust-lang/crates.io-index" 102 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 103 + dependencies = [ 104 + "num-traits", 105 + ] 106 + 107 + [[package]] 108 + name = "atomic-waker" 109 + version = "1.1.2" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 112 + 113 + [[package]] 114 + name = "atproto_teq" 115 + version = "0.1.0" 116 + dependencies = [ 117 + "atrium-api", 118 + "axum", 119 + "axum-extra", 120 + "base64 0.22.1", 121 + "bs58", 122 + "bytes", 123 + "chrono", 124 + "dotenvy", 125 + "embed-resource", 126 + "futures", 127 + "headers", 128 + "hex", 129 + "hmac", 130 + "http", 131 + "jetstream-oxide", 132 + "num", 133 + "num-derive", 134 + "num-traits", 135 + "rand 0.9.0", 136 + "rand_core 0.6.4", 137 + "reqwest", 138 + "secp256k1", 139 + "serde", 140 + "serde_bytes", 141 + "serde_cbor", 142 + "serde_json", 143 + "sha2", 144 + "sqlx", 145 + "tokio", 146 + "tower-http", 147 + "tracing", 148 + "tracing-subscriber", 149 + ] 150 + 151 + [[package]] 152 + name = "atrium-api" 153 + version = "0.24.10" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "9c5d74937642f6b21814e82d80f54d55ebd985b681bffbe27c8a76e726c3c4db" 156 + dependencies = [ 157 + "atrium-xrpc", 158 + "chrono", 159 + "http", 160 + "ipld-core", 161 + "langtag", 162 + "regex", 163 + "serde", 164 + "serde_bytes", 165 + "serde_json", 166 + "thiserror 1.0.69", 167 + "trait-variant", 168 + ] 169 + 170 + [[package]] 171 + name = "atrium-xrpc" 172 + version = "0.12.0" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "f223b98be2acdd7afe5b867744aee8258413ed09993099de0a036b247db0ec4c" 175 + dependencies = [ 176 + "http", 177 + "serde", 178 + "serde_html_form", 179 + "serde_json", 180 + "thiserror 1.0.69", 181 + "trait-variant", 182 + ] 183 + 184 + [[package]] 185 + name = "autocfg" 186 + version = "1.4.0" 187 + source = "registry+https://github.com/rust-lang/crates.io-index" 188 + checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 189 + 190 + [[package]] 191 + name = "axum" 192 + version = "0.8.1" 193 + source = "registry+https://github.com/rust-lang/crates.io-index" 194 + checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" 195 + dependencies = [ 196 + "axum-core", 197 + "base64 0.22.1", 198 + "bytes", 199 + "form_urlencoded", 200 + "futures-util", 201 + "http", 202 + "http-body", 203 + "http-body-util", 204 + "hyper", 205 + "hyper-util", 206 + "itoa", 207 + "matchit", 208 + "memchr", 209 + "mime", 210 + "percent-encoding", 211 + "pin-project-lite", 212 + "rustversion", 213 + "serde", 214 + "serde_json", 215 + "serde_path_to_error", 216 + "serde_urlencoded", 217 + "sha1", 218 + "sync_wrapper", 219 + "tokio", 220 + "tokio-tungstenite 0.26.1", 221 + "tower", 222 + "tower-layer", 223 + "tower-service", 224 + "tracing", 225 + ] 226 + 227 + [[package]] 228 + name = "axum-core" 229 + version = "0.5.0" 230 + source = "registry+https://github.com/rust-lang/crates.io-index" 231 + checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" 232 + dependencies = [ 233 + "bytes", 234 + "futures-util", 235 + "http", 236 + "http-body", 237 + "http-body-util", 238 + "mime", 239 + "pin-project-lite", 240 + "rustversion", 241 + "sync_wrapper", 242 + "tower-layer", 243 + "tower-service", 244 + "tracing", 245 + ] 246 + 247 + [[package]] 248 + name = "axum-extra" 249 + version = "0.10.0" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" 252 + dependencies = [ 253 + "axum", 254 + "axum-core", 255 + "bytes", 256 + "futures-util", 257 + "headers", 258 + "http", 259 + "http-body", 260 + "http-body-util", 261 + "mime", 262 + "pin-project-lite", 263 + "serde", 264 + "tower", 265 + "tower-layer", 266 + "tower-service", 267 + ] 268 + 269 + [[package]] 270 + name = "backtrace" 271 + version = "0.3.74" 272 + source = "registry+https://github.com/rust-lang/crates.io-index" 273 + checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 274 + dependencies = [ 275 + "addr2line", 276 + "cfg-if", 277 + "libc", 278 + "miniz_oxide", 279 + "object", 280 + "rustc-demangle", 281 + "windows-targets 0.52.6", 282 + ] 283 + 284 + [[package]] 285 + name = "base-x" 286 + version = "0.2.11" 287 + source = "registry+https://github.com/rust-lang/crates.io-index" 288 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 289 + 290 + [[package]] 291 + name = "base64" 292 + version = "0.21.7" 293 + source = "registry+https://github.com/rust-lang/crates.io-index" 294 + checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 295 + 296 + [[package]] 297 + name = "base64" 298 + version = "0.22.1" 299 + source = "registry+https://github.com/rust-lang/crates.io-index" 300 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 301 + 302 + [[package]] 303 + name = "base64ct" 304 + version = "1.6.0" 305 + source = "registry+https://github.com/rust-lang/crates.io-index" 306 + checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 307 + 308 + [[package]] 309 + name = "bitcoin-io" 310 + version = "0.1.3" 311 + source = "registry+https://github.com/rust-lang/crates.io-index" 312 + checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" 313 + 314 + [[package]] 315 + name = "bitcoin_hashes" 316 + version = "0.14.0" 317 + source = "registry+https://github.com/rust-lang/crates.io-index" 318 + checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" 319 + dependencies = [ 320 + "bitcoin-io", 321 + "hex-conservative", 322 + ] 323 + 324 + [[package]] 325 + name = "bitflags" 326 + version = "2.8.0" 327 + source = "registry+https://github.com/rust-lang/crates.io-index" 328 + checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 329 + dependencies = [ 330 + "serde", 331 + ] 332 + 333 + [[package]] 334 + name = "block-buffer" 335 + version = "0.10.4" 336 + source = "registry+https://github.com/rust-lang/crates.io-index" 337 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 338 + dependencies = [ 339 + "generic-array", 340 + ] 341 + 342 + [[package]] 343 + name = "brotli" 344 + version = "7.0.0" 345 + source = "registry+https://github.com/rust-lang/crates.io-index" 346 + checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" 347 + dependencies = [ 348 + "alloc-no-stdlib", 349 + "alloc-stdlib", 350 + "brotli-decompressor", 351 + ] 352 + 353 + [[package]] 354 + name = "brotli-decompressor" 355 + version = "4.0.2" 356 + source = "registry+https://github.com/rust-lang/crates.io-index" 357 + checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" 358 + dependencies = [ 359 + "alloc-no-stdlib", 360 + "alloc-stdlib", 361 + ] 362 + 363 + [[package]] 364 + name = "bs58" 365 + version = "0.5.1" 366 + source = "registry+https://github.com/rust-lang/crates.io-index" 367 + checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" 368 + dependencies = [ 369 + "tinyvec", 370 + ] 371 + 372 + [[package]] 373 + name = "bumpalo" 374 + version = "3.17.0" 375 + source = "registry+https://github.com/rust-lang/crates.io-index" 376 + checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 377 + 378 + [[package]] 379 + name = "byteorder" 380 + version = "1.5.0" 381 + source = "registry+https://github.com/rust-lang/crates.io-index" 382 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 383 + 384 + [[package]] 385 + name = "bytes" 386 + version = "1.10.0" 387 + source = "registry+https://github.com/rust-lang/crates.io-index" 388 + checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 389 + 390 + [[package]] 391 + name = "cc" 392 + version = "1.2.10" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" 395 + dependencies = [ 396 + "jobserver", 397 + "libc", 398 + "shlex", 399 + ] 400 + 401 + [[package]] 402 + name = "cfg-if" 403 + version = "1.0.0" 404 + source = "registry+https://github.com/rust-lang/crates.io-index" 405 + checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 406 + 407 + [[package]] 408 + name = "chrono" 409 + version = "0.4.39" 410 + source = "registry+https://github.com/rust-lang/crates.io-index" 411 + checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 412 + dependencies = [ 413 + "android-tzdata", 414 + "iana-time-zone", 415 + "js-sys", 416 + "num-traits", 417 + "serde", 418 + "wasm-bindgen", 419 + "windows-targets 0.52.6", 420 + ] 421 + 422 + [[package]] 423 + name = "cid" 424 + version = "0.11.1" 425 + source = "registry+https://github.com/rust-lang/crates.io-index" 426 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 427 + dependencies = [ 428 + "core2", 429 + "multibase", 430 + "multihash", 431 + "serde", 432 + "serde_bytes", 433 + "unsigned-varint", 434 + ] 435 + 436 + [[package]] 437 + name = "concurrent-queue" 438 + version = "2.5.0" 439 + source = "registry+https://github.com/rust-lang/crates.io-index" 440 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 441 + dependencies = [ 442 + "crossbeam-utils", 443 + ] 444 + 445 + [[package]] 446 + name = "const-oid" 447 + version = "0.9.6" 448 + source = "registry+https://github.com/rust-lang/crates.io-index" 449 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 450 + 451 + [[package]] 452 + name = "core-foundation" 453 + version = "0.9.4" 454 + source = "registry+https://github.com/rust-lang/crates.io-index" 455 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 456 + dependencies = [ 457 + "core-foundation-sys", 458 + "libc", 459 + ] 460 + 461 + [[package]] 462 + name = "core-foundation" 463 + version = "0.10.0" 464 + source = "registry+https://github.com/rust-lang/crates.io-index" 465 + checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" 466 + dependencies = [ 467 + "core-foundation-sys", 468 + "libc", 469 + ] 470 + 471 + [[package]] 472 + name = "core-foundation-sys" 473 + version = "0.8.7" 474 + source = "registry+https://github.com/rust-lang/crates.io-index" 475 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 476 + 477 + [[package]] 478 + name = "core2" 479 + version = "0.4.0" 480 + source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 482 + dependencies = [ 483 + "memchr", 484 + ] 485 + 486 + [[package]] 487 + name = "cpufeatures" 488 + version = "0.2.17" 489 + source = "registry+https://github.com/rust-lang/crates.io-index" 490 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 491 + dependencies = [ 492 + "libc", 493 + ] 494 + 495 + [[package]] 496 + name = "crc" 497 + version = "3.2.1" 498 + source = "registry+https://github.com/rust-lang/crates.io-index" 499 + checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 500 + dependencies = [ 501 + "crc-catalog", 502 + ] 503 + 504 + [[package]] 505 + name = "crc-catalog" 506 + version = "2.4.0" 507 + source = "registry+https://github.com/rust-lang/crates.io-index" 508 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 509 + 510 + [[package]] 511 + name = "crc32fast" 512 + version = "1.4.2" 513 + source = "registry+https://github.com/rust-lang/crates.io-index" 514 + checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 515 + dependencies = [ 516 + "cfg-if", 517 + ] 518 + 519 + [[package]] 520 + name = "crossbeam-queue" 521 + version = "0.3.12" 522 + source = "registry+https://github.com/rust-lang/crates.io-index" 523 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 524 + dependencies = [ 525 + "crossbeam-utils", 526 + ] 527 + 528 + [[package]] 529 + name = "crossbeam-utils" 530 + version = "0.8.21" 531 + source = "registry+https://github.com/rust-lang/crates.io-index" 532 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 533 + 534 + [[package]] 535 + name = "crypto-common" 536 + version = "0.1.6" 537 + source = "registry+https://github.com/rust-lang/crates.io-index" 538 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 539 + dependencies = [ 540 + "generic-array", 541 + "typenum", 542 + ] 543 + 544 + [[package]] 545 + name = "data-encoding" 546 + version = "2.7.0" 547 + source = "registry+https://github.com/rust-lang/crates.io-index" 548 + checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" 549 + 550 + [[package]] 551 + name = "data-encoding-macro" 552 + version = "0.1.16" 553 + source = "registry+https://github.com/rust-lang/crates.io-index" 554 + checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" 555 + dependencies = [ 556 + "data-encoding", 557 + "data-encoding-macro-internal", 558 + ] 559 + 560 + [[package]] 561 + name = "data-encoding-macro-internal" 562 + version = "0.1.14" 563 + source = "registry+https://github.com/rust-lang/crates.io-index" 564 + checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" 565 + dependencies = [ 566 + "data-encoding", 567 + "syn", 568 + ] 569 + 570 + [[package]] 571 + name = "der" 572 + version = "0.7.9" 573 + source = "registry+https://github.com/rust-lang/crates.io-index" 574 + checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 575 + dependencies = [ 576 + "const-oid", 577 + "pem-rfc7468", 578 + "zeroize", 579 + ] 580 + 581 + [[package]] 582 + name = "digest" 583 + version = "0.10.7" 584 + source = "registry+https://github.com/rust-lang/crates.io-index" 585 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 586 + dependencies = [ 587 + "block-buffer", 588 + "const-oid", 589 + "crypto-common", 590 + "subtle", 591 + ] 592 + 593 + [[package]] 594 + name = "displaydoc" 595 + version = "0.2.5" 596 + source = "registry+https://github.com/rust-lang/crates.io-index" 597 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 598 + dependencies = [ 599 + "proc-macro2", 600 + "quote", 601 + "syn", 602 + ] 603 + 604 + [[package]] 605 + name = "dotenvy" 606 + version = "0.15.7" 607 + source = "registry+https://github.com/rust-lang/crates.io-index" 608 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 609 + 610 + [[package]] 611 + name = "either" 612 + version = "1.13.0" 613 + source = "registry+https://github.com/rust-lang/crates.io-index" 614 + checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 615 + dependencies = [ 616 + "serde", 617 + ] 618 + 619 + [[package]] 620 + name = "embed-resource" 621 + version = "1.8.0" 622 + source = "registry+https://github.com/rust-lang/crates.io-index" 623 + checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" 624 + dependencies = [ 625 + "cc", 626 + "rustc_version", 627 + "toml", 628 + "vswhom", 629 + "winreg", 630 + ] 631 + 632 + [[package]] 633 + name = "encoding_rs" 634 + version = "0.8.35" 635 + source = "registry+https://github.com/rust-lang/crates.io-index" 636 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 637 + dependencies = [ 638 + "cfg-if", 639 + ] 640 + 641 + [[package]] 642 + name = "equivalent" 643 + version = "1.0.1" 644 + source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 646 + 647 + [[package]] 648 + name = "errno" 649 + version = "0.3.10" 650 + source = "registry+https://github.com/rust-lang/crates.io-index" 651 + checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 652 + dependencies = [ 653 + "libc", 654 + "windows-sys 0.59.0", 655 + ] 656 + 657 + [[package]] 658 + name = "etcetera" 659 + version = "0.8.0" 660 + source = "registry+https://github.com/rust-lang/crates.io-index" 661 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 662 + dependencies = [ 663 + "cfg-if", 664 + "home", 665 + "windows-sys 0.48.0", 666 + ] 667 + 668 + [[package]] 669 + name = "event-listener" 670 + version = "5.4.0" 671 + source = "registry+https://github.com/rust-lang/crates.io-index" 672 + checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" 673 + dependencies = [ 674 + "concurrent-queue", 675 + "parking", 676 + "pin-project-lite", 677 + ] 678 + 679 + [[package]] 680 + name = "fastrand" 681 + version = "2.3.0" 682 + source = "registry+https://github.com/rust-lang/crates.io-index" 683 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 684 + 685 + [[package]] 686 + name = "flate2" 687 + version = "1.0.35" 688 + source = "registry+https://github.com/rust-lang/crates.io-index" 689 + checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 690 + dependencies = [ 691 + "crc32fast", 692 + "miniz_oxide", 693 + ] 694 + 695 + [[package]] 696 + name = "flume" 697 + version = "0.11.1" 698 + source = "registry+https://github.com/rust-lang/crates.io-index" 699 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 700 + dependencies = [ 701 + "futures-core", 702 + "futures-sink", 703 + "nanorand", 704 + "spin", 705 + ] 706 + 707 + [[package]] 708 + name = "fnv" 709 + version = "1.0.7" 710 + source = "registry+https://github.com/rust-lang/crates.io-index" 711 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 712 + 713 + [[package]] 714 + name = "foldhash" 715 + version = "0.1.4" 716 + source = "registry+https://github.com/rust-lang/crates.io-index" 717 + checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 718 + 719 + [[package]] 720 + name = "foreign-types" 721 + version = "0.3.2" 722 + source = "registry+https://github.com/rust-lang/crates.io-index" 723 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 724 + dependencies = [ 725 + "foreign-types-shared", 726 + ] 727 + 728 + [[package]] 729 + name = "foreign-types-shared" 730 + version = "0.1.1" 731 + source = "registry+https://github.com/rust-lang/crates.io-index" 732 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 733 + 734 + [[package]] 735 + name = "form_urlencoded" 736 + version = "1.2.1" 737 + source = "registry+https://github.com/rust-lang/crates.io-index" 738 + checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 739 + dependencies = [ 740 + "percent-encoding", 741 + ] 742 + 743 + [[package]] 744 + name = "futures" 745 + version = "0.3.31" 746 + source = "registry+https://github.com/rust-lang/crates.io-index" 747 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 748 + dependencies = [ 749 + "futures-channel", 750 + "futures-core", 751 + "futures-executor", 752 + "futures-io", 753 + "futures-sink", 754 + "futures-task", 755 + "futures-util", 756 + ] 757 + 758 + [[package]] 759 + name = "futures-channel" 760 + version = "0.3.31" 761 + source = "registry+https://github.com/rust-lang/crates.io-index" 762 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 763 + dependencies = [ 764 + "futures-core", 765 + "futures-sink", 766 + ] 767 + 768 + [[package]] 769 + name = "futures-core" 770 + version = "0.3.31" 771 + source = "registry+https://github.com/rust-lang/crates.io-index" 772 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 773 + 774 + [[package]] 775 + name = "futures-executor" 776 + version = "0.3.31" 777 + source = "registry+https://github.com/rust-lang/crates.io-index" 778 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 779 + dependencies = [ 780 + "futures-core", 781 + "futures-task", 782 + "futures-util", 783 + ] 784 + 785 + [[package]] 786 + name = "futures-intrusive" 787 + version = "0.5.0" 788 + source = "registry+https://github.com/rust-lang/crates.io-index" 789 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 790 + dependencies = [ 791 + "futures-core", 792 + "lock_api", 793 + "parking_lot", 794 + ] 795 + 796 + [[package]] 797 + name = "futures-io" 798 + version = "0.3.31" 799 + source = "registry+https://github.com/rust-lang/crates.io-index" 800 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 801 + 802 + [[package]] 803 + name = "futures-macro" 804 + version = "0.3.31" 805 + source = "registry+https://github.com/rust-lang/crates.io-index" 806 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 807 + dependencies = [ 808 + "proc-macro2", 809 + "quote", 810 + "syn", 811 + ] 812 + 813 + [[package]] 814 + name = "futures-sink" 815 + version = "0.3.31" 816 + source = "registry+https://github.com/rust-lang/crates.io-index" 817 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 818 + 819 + [[package]] 820 + name = "futures-task" 821 + version = "0.3.31" 822 + source = "registry+https://github.com/rust-lang/crates.io-index" 823 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 824 + 825 + [[package]] 826 + name = "futures-util" 827 + version = "0.3.31" 828 + source = "registry+https://github.com/rust-lang/crates.io-index" 829 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 830 + dependencies = [ 831 + "futures-channel", 832 + "futures-core", 833 + "futures-io", 834 + "futures-macro", 835 + "futures-sink", 836 + "futures-task", 837 + "memchr", 838 + "pin-project-lite", 839 + "pin-utils", 840 + "slab", 841 + ] 842 + 843 + [[package]] 844 + name = "generic-array" 845 + version = "0.14.7" 846 + source = "registry+https://github.com/rust-lang/crates.io-index" 847 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 848 + dependencies = [ 849 + "typenum", 850 + "version_check", 851 + ] 852 + 853 + [[package]] 854 + name = "getrandom" 855 + version = "0.2.15" 856 + source = "registry+https://github.com/rust-lang/crates.io-index" 857 + checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 858 + dependencies = [ 859 + "cfg-if", 860 + "js-sys", 861 + "libc", 862 + "wasi 0.11.0+wasi-snapshot-preview1", 863 + "wasm-bindgen", 864 + ] 865 + 866 + [[package]] 867 + name = "getrandom" 868 + version = "0.3.1" 869 + source = "registry+https://github.com/rust-lang/crates.io-index" 870 + checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 871 + dependencies = [ 872 + "cfg-if", 873 + "libc", 874 + "wasi 0.13.3+wasi-0.2.2", 875 + "windows-targets 0.52.6", 876 + ] 877 + 878 + [[package]] 879 + name = "gimli" 880 + version = "0.31.1" 881 + source = "registry+https://github.com/rust-lang/crates.io-index" 882 + checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 883 + 884 + [[package]] 885 + name = "h2" 886 + version = "0.4.7" 887 + source = "registry+https://github.com/rust-lang/crates.io-index" 888 + checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" 889 + dependencies = [ 890 + "atomic-waker", 891 + "bytes", 892 + "fnv", 893 + "futures-core", 894 + "futures-sink", 895 + "http", 896 + "indexmap", 897 + "slab", 898 + "tokio", 899 + "tokio-util", 900 + "tracing", 901 + ] 902 + 903 + [[package]] 904 + name = "half" 905 + version = "1.8.3" 906 + source = "registry+https://github.com/rust-lang/crates.io-index" 907 + checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" 908 + 909 + [[package]] 910 + name = "hashbrown" 911 + version = "0.15.2" 912 + source = "registry+https://github.com/rust-lang/crates.io-index" 913 + checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 914 + dependencies = [ 915 + "allocator-api2", 916 + "equivalent", 917 + "foldhash", 918 + ] 919 + 920 + [[package]] 921 + name = "hashlink" 922 + version = "0.10.0" 923 + source = "registry+https://github.com/rust-lang/crates.io-index" 924 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 925 + dependencies = [ 926 + "hashbrown", 927 + ] 928 + 929 + [[package]] 930 + name = "headers" 931 + version = "0.4.0" 932 + source = "registry+https://github.com/rust-lang/crates.io-index" 933 + checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" 934 + dependencies = [ 935 + "base64 0.21.7", 936 + "bytes", 937 + "headers-core", 938 + "http", 939 + "httpdate", 940 + "mime", 941 + "sha1", 942 + ] 943 + 944 + [[package]] 945 + name = "headers-core" 946 + version = "0.3.0" 947 + source = "registry+https://github.com/rust-lang/crates.io-index" 948 + checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" 949 + dependencies = [ 950 + "http", 951 + ] 952 + 953 + [[package]] 954 + name = "heck" 955 + version = "0.5.0" 956 + source = "registry+https://github.com/rust-lang/crates.io-index" 957 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 958 + 959 + [[package]] 960 + name = "hex" 961 + version = "0.4.3" 962 + source = "registry+https://github.com/rust-lang/crates.io-index" 963 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 964 + 965 + [[package]] 966 + name = "hex-conservative" 967 + version = "0.2.1" 968 + source = "registry+https://github.com/rust-lang/crates.io-index" 969 + checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" 970 + dependencies = [ 971 + "arrayvec", 972 + ] 973 + 974 + [[package]] 975 + name = "hkdf" 976 + version = "0.12.4" 977 + source = "registry+https://github.com/rust-lang/crates.io-index" 978 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 979 + dependencies = [ 980 + "hmac", 981 + ] 982 + 983 + [[package]] 984 + name = "hmac" 985 + version = "0.12.1" 986 + source = "registry+https://github.com/rust-lang/crates.io-index" 987 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 988 + dependencies = [ 989 + "digest", 990 + ] 991 + 992 + [[package]] 993 + name = "home" 994 + version = "0.5.11" 995 + source = "registry+https://github.com/rust-lang/crates.io-index" 996 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 997 + dependencies = [ 998 + "windows-sys 0.59.0", 999 + ] 1000 + 1001 + [[package]] 1002 + name = "http" 1003 + version = "1.2.0" 1004 + source = "registry+https://github.com/rust-lang/crates.io-index" 1005 + checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 1006 + dependencies = [ 1007 + "bytes", 1008 + "fnv", 1009 + "itoa", 1010 + ] 1011 + 1012 + [[package]] 1013 + name = "http-body" 1014 + version = "1.0.1" 1015 + source = "registry+https://github.com/rust-lang/crates.io-index" 1016 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1017 + dependencies = [ 1018 + "bytes", 1019 + "http", 1020 + ] 1021 + 1022 + [[package]] 1023 + name = "http-body-util" 1024 + version = "0.1.2" 1025 + source = "registry+https://github.com/rust-lang/crates.io-index" 1026 + checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 1027 + dependencies = [ 1028 + "bytes", 1029 + "futures-util", 1030 + "http", 1031 + "http-body", 1032 + "pin-project-lite", 1033 + ] 1034 + 1035 + [[package]] 1036 + name = "http-range-header" 1037 + version = "0.4.2" 1038 + source = "registry+https://github.com/rust-lang/crates.io-index" 1039 + checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 1040 + 1041 + [[package]] 1042 + name = "httparse" 1043 + version = "1.10.0" 1044 + source = "registry+https://github.com/rust-lang/crates.io-index" 1045 + checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 1046 + 1047 + [[package]] 1048 + name = "httpdate" 1049 + version = "1.0.3" 1050 + source = "registry+https://github.com/rust-lang/crates.io-index" 1051 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1052 + 1053 + [[package]] 1054 + name = "hyper" 1055 + version = "1.6.0" 1056 + source = "registry+https://github.com/rust-lang/crates.io-index" 1057 + checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 1058 + dependencies = [ 1059 + "bytes", 1060 + "futures-channel", 1061 + "futures-util", 1062 + "h2", 1063 + "http", 1064 + "http-body", 1065 + "httparse", 1066 + "httpdate", 1067 + "itoa", 1068 + "pin-project-lite", 1069 + "smallvec", 1070 + "tokio", 1071 + "want", 1072 + ] 1073 + 1074 + [[package]] 1075 + name = "hyper-rustls" 1076 + version = "0.27.5" 1077 + source = "registry+https://github.com/rust-lang/crates.io-index" 1078 + checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 1079 + dependencies = [ 1080 + "futures-util", 1081 + "http", 1082 + "hyper", 1083 + "hyper-util", 1084 + "rustls", 1085 + "rustls-pki-types", 1086 + "tokio", 1087 + "tokio-rustls", 1088 + "tower-service", 1089 + ] 1090 + 1091 + [[package]] 1092 + name = "hyper-tls" 1093 + version = "0.6.0" 1094 + source = "registry+https://github.com/rust-lang/crates.io-index" 1095 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1096 + dependencies = [ 1097 + "bytes", 1098 + "http-body-util", 1099 + "hyper", 1100 + "hyper-util", 1101 + "native-tls", 1102 + "tokio", 1103 + "tokio-native-tls", 1104 + "tower-service", 1105 + ] 1106 + 1107 + [[package]] 1108 + name = "hyper-util" 1109 + version = "0.1.10" 1110 + source = "registry+https://github.com/rust-lang/crates.io-index" 1111 + checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 1112 + dependencies = [ 1113 + "bytes", 1114 + "futures-channel", 1115 + "futures-util", 1116 + "http", 1117 + "http-body", 1118 + "hyper", 1119 + "pin-project-lite", 1120 + "socket2", 1121 + "tokio", 1122 + "tower-service", 1123 + "tracing", 1124 + ] 1125 + 1126 + [[package]] 1127 + name = "iana-time-zone" 1128 + version = "0.1.61" 1129 + source = "registry+https://github.com/rust-lang/crates.io-index" 1130 + checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 1131 + dependencies = [ 1132 + "android_system_properties", 1133 + "core-foundation-sys", 1134 + "iana-time-zone-haiku", 1135 + "js-sys", 1136 + "wasm-bindgen", 1137 + "windows-core", 1138 + ] 1139 + 1140 + [[package]] 1141 + name = "iana-time-zone-haiku" 1142 + version = "0.1.2" 1143 + source = "registry+https://github.com/rust-lang/crates.io-index" 1144 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1145 + dependencies = [ 1146 + "cc", 1147 + ] 1148 + 1149 + [[package]] 1150 + name = "icu_collections" 1151 + version = "1.5.0" 1152 + source = "registry+https://github.com/rust-lang/crates.io-index" 1153 + checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 1154 + dependencies = [ 1155 + "displaydoc", 1156 + "yoke", 1157 + "zerofrom", 1158 + "zerovec", 1159 + ] 1160 + 1161 + [[package]] 1162 + name = "icu_locid" 1163 + version = "1.5.0" 1164 + source = "registry+https://github.com/rust-lang/crates.io-index" 1165 + checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 1166 + dependencies = [ 1167 + "displaydoc", 1168 + "litemap", 1169 + "tinystr", 1170 + "writeable", 1171 + "zerovec", 1172 + ] 1173 + 1174 + [[package]] 1175 + name = "icu_locid_transform" 1176 + version = "1.5.0" 1177 + source = "registry+https://github.com/rust-lang/crates.io-index" 1178 + checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 1179 + dependencies = [ 1180 + "displaydoc", 1181 + "icu_locid", 1182 + "icu_locid_transform_data", 1183 + "icu_provider", 1184 + "tinystr", 1185 + "zerovec", 1186 + ] 1187 + 1188 + [[package]] 1189 + name = "icu_locid_transform_data" 1190 + version = "1.5.0" 1191 + source = "registry+https://github.com/rust-lang/crates.io-index" 1192 + checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 1193 + 1194 + [[package]] 1195 + name = "icu_normalizer" 1196 + version = "1.5.0" 1197 + source = "registry+https://github.com/rust-lang/crates.io-index" 1198 + checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 1199 + dependencies = [ 1200 + "displaydoc", 1201 + "icu_collections", 1202 + "icu_normalizer_data", 1203 + "icu_properties", 1204 + "icu_provider", 1205 + "smallvec", 1206 + "utf16_iter", 1207 + "utf8_iter", 1208 + "write16", 1209 + "zerovec", 1210 + ] 1211 + 1212 + [[package]] 1213 + name = "icu_normalizer_data" 1214 + version = "1.5.0" 1215 + source = "registry+https://github.com/rust-lang/crates.io-index" 1216 + checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 1217 + 1218 + [[package]] 1219 + name = "icu_properties" 1220 + version = "1.5.1" 1221 + source = "registry+https://github.com/rust-lang/crates.io-index" 1222 + checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 1223 + dependencies = [ 1224 + "displaydoc", 1225 + "icu_collections", 1226 + "icu_locid_transform", 1227 + "icu_properties_data", 1228 + "icu_provider", 1229 + "tinystr", 1230 + "zerovec", 1231 + ] 1232 + 1233 + [[package]] 1234 + name = "icu_properties_data" 1235 + version = "1.5.0" 1236 + source = "registry+https://github.com/rust-lang/crates.io-index" 1237 + checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 1238 + 1239 + [[package]] 1240 + name = "icu_provider" 1241 + version = "1.5.0" 1242 + source = "registry+https://github.com/rust-lang/crates.io-index" 1243 + checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 1244 + dependencies = [ 1245 + "displaydoc", 1246 + "icu_locid", 1247 + "icu_provider_macros", 1248 + "stable_deref_trait", 1249 + "tinystr", 1250 + "writeable", 1251 + "yoke", 1252 + "zerofrom", 1253 + "zerovec", 1254 + ] 1255 + 1256 + [[package]] 1257 + name = "icu_provider_macros" 1258 + version = "1.5.0" 1259 + source = "registry+https://github.com/rust-lang/crates.io-index" 1260 + checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 1261 + dependencies = [ 1262 + "proc-macro2", 1263 + "quote", 1264 + "syn", 1265 + ] 1266 + 1267 + [[package]] 1268 + name = "idna" 1269 + version = "1.0.3" 1270 + source = "registry+https://github.com/rust-lang/crates.io-index" 1271 + checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1272 + dependencies = [ 1273 + "idna_adapter", 1274 + "smallvec", 1275 + "utf8_iter", 1276 + ] 1277 + 1278 + [[package]] 1279 + name = "idna_adapter" 1280 + version = "1.2.0" 1281 + source = "registry+https://github.com/rust-lang/crates.io-index" 1282 + checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 1283 + dependencies = [ 1284 + "icu_normalizer", 1285 + "icu_properties", 1286 + ] 1287 + 1288 + [[package]] 1289 + name = "indexmap" 1290 + version = "2.7.1" 1291 + source = "registry+https://github.com/rust-lang/crates.io-index" 1292 + checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 1293 + dependencies = [ 1294 + "equivalent", 1295 + "hashbrown", 1296 + ] 1297 + 1298 + [[package]] 1299 + name = "ipld-core" 1300 + version = "0.4.1" 1301 + source = "registry+https://github.com/rust-lang/crates.io-index" 1302 + checksum = "b4ede82a79e134f179f4b29b5fdb1eb92bd1b38c4dfea394c539051150a21b9b" 1303 + dependencies = [ 1304 + "cid", 1305 + "serde", 1306 + "serde_bytes", 1307 + ] 1308 + 1309 + [[package]] 1310 + name = "ipnet" 1311 + version = "2.11.0" 1312 + source = "registry+https://github.com/rust-lang/crates.io-index" 1313 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1314 + 1315 + [[package]] 1316 + name = "itoa" 1317 + version = "1.0.14" 1318 + source = "registry+https://github.com/rust-lang/crates.io-index" 1319 + checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 1320 + 1321 + [[package]] 1322 + name = "jetstream-oxide" 1323 + version = "0.1.1" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "cbde770c76b32262a837d968850d29232189844f4e11a3f02a3d87473929f7e9" 1326 + dependencies = [ 1327 + "async-trait", 1328 + "atrium-api", 1329 + "chrono", 1330 + "flume", 1331 + "futures-util", 1332 + "log", 1333 + "serde", 1334 + "serde_json", 1335 + "thiserror 2.0.11", 1336 + "tokio", 1337 + "tokio-tungstenite 0.24.0", 1338 + "tokio-util", 1339 + "url", 1340 + "zstd", 1341 + ] 1342 + 1343 + [[package]] 1344 + name = "jobserver" 1345 + version = "0.1.32" 1346 + source = "registry+https://github.com/rust-lang/crates.io-index" 1347 + checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 1348 + dependencies = [ 1349 + "libc", 1350 + ] 1351 + 1352 + [[package]] 1353 + name = "js-sys" 1354 + version = "0.3.77" 1355 + source = "registry+https://github.com/rust-lang/crates.io-index" 1356 + checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1357 + dependencies = [ 1358 + "once_cell", 1359 + "wasm-bindgen", 1360 + ] 1361 + 1362 + [[package]] 1363 + name = "langtag" 1364 + version = "0.3.4" 1365 + source = "registry+https://github.com/rust-lang/crates.io-index" 1366 + checksum = "ed60c85f254d6ae8450cec15eedd921efbc4d1bdf6fcf6202b9a58b403f6f805" 1367 + dependencies = [ 1368 + "serde", 1369 + ] 1370 + 1371 + [[package]] 1372 + name = "lazy_static" 1373 + version = "1.5.0" 1374 + source = "registry+https://github.com/rust-lang/crates.io-index" 1375 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1376 + dependencies = [ 1377 + "spin", 1378 + ] 1379 + 1380 + [[package]] 1381 + name = "libc" 1382 + version = "0.2.169" 1383 + source = "registry+https://github.com/rust-lang/crates.io-index" 1384 + checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 1385 + 1386 + [[package]] 1387 + name = "libm" 1388 + version = "0.2.11" 1389 + source = "registry+https://github.com/rust-lang/crates.io-index" 1390 + checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 1391 + 1392 + [[package]] 1393 + name = "libsqlite3-sys" 1394 + version = "0.30.1" 1395 + source = "registry+https://github.com/rust-lang/crates.io-index" 1396 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1397 + dependencies = [ 1398 + "cc", 1399 + "pkg-config", 1400 + "vcpkg", 1401 + ] 1402 + 1403 + [[package]] 1404 + name = "linux-raw-sys" 1405 + version = "0.4.15" 1406 + source = "registry+https://github.com/rust-lang/crates.io-index" 1407 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 1408 + 1409 + [[package]] 1410 + name = "litemap" 1411 + version = "0.7.4" 1412 + source = "registry+https://github.com/rust-lang/crates.io-index" 1413 + checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 1414 + 1415 + [[package]] 1416 + name = "lock_api" 1417 + version = "0.4.12" 1418 + source = "registry+https://github.com/rust-lang/crates.io-index" 1419 + checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1420 + dependencies = [ 1421 + "autocfg", 1422 + "scopeguard", 1423 + ] 1424 + 1425 + [[package]] 1426 + name = "log" 1427 + version = "0.4.25" 1428 + source = "registry+https://github.com/rust-lang/crates.io-index" 1429 + checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 1430 + 1431 + [[package]] 1432 + name = "matchers" 1433 + version = "0.1.0" 1434 + source = "registry+https://github.com/rust-lang/crates.io-index" 1435 + checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1436 + dependencies = [ 1437 + "regex-automata 0.1.10", 1438 + ] 1439 + 1440 + [[package]] 1441 + name = "matchit" 1442 + version = "0.8.4" 1443 + source = "registry+https://github.com/rust-lang/crates.io-index" 1444 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1445 + 1446 + [[package]] 1447 + name = "md-5" 1448 + version = "0.10.6" 1449 + source = "registry+https://github.com/rust-lang/crates.io-index" 1450 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1451 + dependencies = [ 1452 + "cfg-if", 1453 + "digest", 1454 + ] 1455 + 1456 + [[package]] 1457 + name = "memchr" 1458 + version = "2.7.4" 1459 + source = "registry+https://github.com/rust-lang/crates.io-index" 1460 + checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1461 + 1462 + [[package]] 1463 + name = "mime" 1464 + version = "0.3.17" 1465 + source = "registry+https://github.com/rust-lang/crates.io-index" 1466 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1467 + 1468 + [[package]] 1469 + name = "mime_guess" 1470 + version = "2.0.5" 1471 + source = "registry+https://github.com/rust-lang/crates.io-index" 1472 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1473 + dependencies = [ 1474 + "mime", 1475 + "unicase", 1476 + ] 1477 + 1478 + [[package]] 1479 + name = "miniz_oxide" 1480 + version = "0.8.3" 1481 + source = "registry+https://github.com/rust-lang/crates.io-index" 1482 + checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" 1483 + dependencies = [ 1484 + "adler2", 1485 + ] 1486 + 1487 + [[package]] 1488 + name = "mio" 1489 + version = "1.0.3" 1490 + source = "registry+https://github.com/rust-lang/crates.io-index" 1491 + checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1492 + dependencies = [ 1493 + "libc", 1494 + "wasi 0.11.0+wasi-snapshot-preview1", 1495 + "windows-sys 0.52.0", 1496 + ] 1497 + 1498 + [[package]] 1499 + name = "multibase" 1500 + version = "0.9.1" 1501 + source = "registry+https://github.com/rust-lang/crates.io-index" 1502 + checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" 1503 + dependencies = [ 1504 + "base-x", 1505 + "data-encoding", 1506 + "data-encoding-macro", 1507 + ] 1508 + 1509 + [[package]] 1510 + name = "multihash" 1511 + version = "0.19.3" 1512 + source = "registry+https://github.com/rust-lang/crates.io-index" 1513 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1514 + dependencies = [ 1515 + "core2", 1516 + "serde", 1517 + "unsigned-varint", 1518 + ] 1519 + 1520 + [[package]] 1521 + name = "nanorand" 1522 + version = "0.7.0" 1523 + source = "registry+https://github.com/rust-lang/crates.io-index" 1524 + checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 1525 + dependencies = [ 1526 + "getrandom 0.2.15", 1527 + ] 1528 + 1529 + [[package]] 1530 + name = "native-tls" 1531 + version = "0.2.13" 1532 + source = "registry+https://github.com/rust-lang/crates.io-index" 1533 + checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" 1534 + dependencies = [ 1535 + "libc", 1536 + "log", 1537 + "openssl", 1538 + "openssl-probe", 1539 + "openssl-sys", 1540 + "schannel", 1541 + "security-framework 2.11.1", 1542 + "security-framework-sys", 1543 + "tempfile", 1544 + ] 1545 + 1546 + [[package]] 1547 + name = "nu-ansi-term" 1548 + version = "0.46.0" 1549 + source = "registry+https://github.com/rust-lang/crates.io-index" 1550 + checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1551 + dependencies = [ 1552 + "overload", 1553 + "winapi", 1554 + ] 1555 + 1556 + [[package]] 1557 + name = "num" 1558 + version = "0.4.3" 1559 + source = "registry+https://github.com/rust-lang/crates.io-index" 1560 + checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 1561 + dependencies = [ 1562 + "num-bigint", 1563 + "num-complex", 1564 + "num-integer", 1565 + "num-iter", 1566 + "num-rational", 1567 + "num-traits", 1568 + ] 1569 + 1570 + [[package]] 1571 + name = "num-bigint" 1572 + version = "0.4.6" 1573 + source = "registry+https://github.com/rust-lang/crates.io-index" 1574 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 1575 + dependencies = [ 1576 + "num-integer", 1577 + "num-traits", 1578 + ] 1579 + 1580 + [[package]] 1581 + name = "num-bigint-dig" 1582 + version = "0.8.4" 1583 + source = "registry+https://github.com/rust-lang/crates.io-index" 1584 + checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 1585 + dependencies = [ 1586 + "byteorder", 1587 + "lazy_static", 1588 + "libm", 1589 + "num-integer", 1590 + "num-iter", 1591 + "num-traits", 1592 + "rand 0.8.5", 1593 + "smallvec", 1594 + "zeroize", 1595 + ] 1596 + 1597 + [[package]] 1598 + name = "num-complex" 1599 + version = "0.4.6" 1600 + source = "registry+https://github.com/rust-lang/crates.io-index" 1601 + checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 1602 + dependencies = [ 1603 + "num-traits", 1604 + ] 1605 + 1606 + [[package]] 1607 + name = "num-derive" 1608 + version = "0.4.2" 1609 + source = "registry+https://github.com/rust-lang/crates.io-index" 1610 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 1611 + dependencies = [ 1612 + "proc-macro2", 1613 + "quote", 1614 + "syn", 1615 + ] 1616 + 1617 + [[package]] 1618 + name = "num-integer" 1619 + version = "0.1.46" 1620 + source = "registry+https://github.com/rust-lang/crates.io-index" 1621 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1622 + dependencies = [ 1623 + "num-traits", 1624 + ] 1625 + 1626 + [[package]] 1627 + name = "num-iter" 1628 + version = "0.1.45" 1629 + source = "registry+https://github.com/rust-lang/crates.io-index" 1630 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1631 + dependencies = [ 1632 + "autocfg", 1633 + "num-integer", 1634 + "num-traits", 1635 + ] 1636 + 1637 + [[package]] 1638 + name = "num-rational" 1639 + version = "0.4.2" 1640 + source = "registry+https://github.com/rust-lang/crates.io-index" 1641 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 1642 + dependencies = [ 1643 + "num-bigint", 1644 + "num-integer", 1645 + "num-traits", 1646 + ] 1647 + 1648 + [[package]] 1649 + name = "num-traits" 1650 + version = "0.2.19" 1651 + source = "registry+https://github.com/rust-lang/crates.io-index" 1652 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1653 + dependencies = [ 1654 + "autocfg", 1655 + "libm", 1656 + ] 1657 + 1658 + [[package]] 1659 + name = "object" 1660 + version = "0.36.7" 1661 + source = "registry+https://github.com/rust-lang/crates.io-index" 1662 + checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1663 + dependencies = [ 1664 + "memchr", 1665 + ] 1666 + 1667 + [[package]] 1668 + name = "once_cell" 1669 + version = "1.20.2" 1670 + source = "registry+https://github.com/rust-lang/crates.io-index" 1671 + checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 1672 + 1673 + [[package]] 1674 + name = "openssl" 1675 + version = "0.10.69" 1676 + source = "registry+https://github.com/rust-lang/crates.io-index" 1677 + checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" 1678 + dependencies = [ 1679 + "bitflags", 1680 + "cfg-if", 1681 + "foreign-types", 1682 + "libc", 1683 + "once_cell", 1684 + "openssl-macros", 1685 + "openssl-sys", 1686 + ] 1687 + 1688 + [[package]] 1689 + name = "openssl-macros" 1690 + version = "0.1.1" 1691 + source = "registry+https://github.com/rust-lang/crates.io-index" 1692 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1693 + dependencies = [ 1694 + "proc-macro2", 1695 + "quote", 1696 + "syn", 1697 + ] 1698 + 1699 + [[package]] 1700 + name = "openssl-probe" 1701 + version = "0.1.6" 1702 + source = "registry+https://github.com/rust-lang/crates.io-index" 1703 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1704 + 1705 + [[package]] 1706 + name = "openssl-src" 1707 + version = "300.4.1+3.4.0" 1708 + source = "registry+https://github.com/rust-lang/crates.io-index" 1709 + checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" 1710 + dependencies = [ 1711 + "cc", 1712 + ] 1713 + 1714 + [[package]] 1715 + name = "openssl-sys" 1716 + version = "0.9.104" 1717 + source = "registry+https://github.com/rust-lang/crates.io-index" 1718 + checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" 1719 + dependencies = [ 1720 + "cc", 1721 + "libc", 1722 + "openssl-src", 1723 + "pkg-config", 1724 + "vcpkg", 1725 + ] 1726 + 1727 + [[package]] 1728 + name = "overload" 1729 + version = "0.1.1" 1730 + source = "registry+https://github.com/rust-lang/crates.io-index" 1731 + checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1732 + 1733 + [[package]] 1734 + name = "parking" 1735 + version = "2.2.1" 1736 + source = "registry+https://github.com/rust-lang/crates.io-index" 1737 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1738 + 1739 + [[package]] 1740 + name = "parking_lot" 1741 + version = "0.12.3" 1742 + source = "registry+https://github.com/rust-lang/crates.io-index" 1743 + checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1744 + dependencies = [ 1745 + "lock_api", 1746 + "parking_lot_core", 1747 + ] 1748 + 1749 + [[package]] 1750 + name = "parking_lot_core" 1751 + version = "0.9.10" 1752 + source = "registry+https://github.com/rust-lang/crates.io-index" 1753 + checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1754 + dependencies = [ 1755 + "cfg-if", 1756 + "libc", 1757 + "redox_syscall", 1758 + "smallvec", 1759 + "windows-targets 0.52.6", 1760 + ] 1761 + 1762 + [[package]] 1763 + name = "pem-rfc7468" 1764 + version = "0.7.0" 1765 + source = "registry+https://github.com/rust-lang/crates.io-index" 1766 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1767 + dependencies = [ 1768 + "base64ct", 1769 + ] 1770 + 1771 + [[package]] 1772 + name = "percent-encoding" 1773 + version = "2.3.1" 1774 + source = "registry+https://github.com/rust-lang/crates.io-index" 1775 + checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1776 + 1777 + [[package]] 1778 + name = "pin-project-lite" 1779 + version = "0.2.16" 1780 + source = "registry+https://github.com/rust-lang/crates.io-index" 1781 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1782 + 1783 + [[package]] 1784 + name = "pin-utils" 1785 + version = "0.1.0" 1786 + source = "registry+https://github.com/rust-lang/crates.io-index" 1787 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1788 + 1789 + [[package]] 1790 + name = "pkcs1" 1791 + version = "0.7.5" 1792 + source = "registry+https://github.com/rust-lang/crates.io-index" 1793 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1794 + dependencies = [ 1795 + "der", 1796 + "pkcs8", 1797 + "spki", 1798 + ] 1799 + 1800 + [[package]] 1801 + name = "pkcs8" 1802 + version = "0.10.2" 1803 + source = "registry+https://github.com/rust-lang/crates.io-index" 1804 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1805 + dependencies = [ 1806 + "der", 1807 + "spki", 1808 + ] 1809 + 1810 + [[package]] 1811 + name = "pkg-config" 1812 + version = "0.3.31" 1813 + source = "registry+https://github.com/rust-lang/crates.io-index" 1814 + checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1815 + 1816 + [[package]] 1817 + name = "ppv-lite86" 1818 + version = "0.2.20" 1819 + source = "registry+https://github.com/rust-lang/crates.io-index" 1820 + checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1821 + dependencies = [ 1822 + "zerocopy 0.7.35", 1823 + ] 1824 + 1825 + [[package]] 1826 + name = "proc-macro2" 1827 + version = "1.0.93" 1828 + source = "registry+https://github.com/rust-lang/crates.io-index" 1829 + checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1830 + dependencies = [ 1831 + "unicode-ident", 1832 + ] 1833 + 1834 + [[package]] 1835 + name = "quote" 1836 + version = "1.0.38" 1837 + source = "registry+https://github.com/rust-lang/crates.io-index" 1838 + checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1839 + dependencies = [ 1840 + "proc-macro2", 1841 + ] 1842 + 1843 + [[package]] 1844 + name = "rand" 1845 + version = "0.8.5" 1846 + source = "registry+https://github.com/rust-lang/crates.io-index" 1847 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1848 + dependencies = [ 1849 + "libc", 1850 + "rand_chacha 0.3.1", 1851 + "rand_core 0.6.4", 1852 + ] 1853 + 1854 + [[package]] 1855 + name = "rand" 1856 + version = "0.9.0" 1857 + source = "registry+https://github.com/rust-lang/crates.io-index" 1858 + checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 1859 + dependencies = [ 1860 + "rand_chacha 0.9.0", 1861 + "rand_core 0.9.0", 1862 + "zerocopy 0.8.17", 1863 + ] 1864 + 1865 + [[package]] 1866 + name = "rand_chacha" 1867 + version = "0.3.1" 1868 + source = "registry+https://github.com/rust-lang/crates.io-index" 1869 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1870 + dependencies = [ 1871 + "ppv-lite86", 1872 + "rand_core 0.6.4", 1873 + ] 1874 + 1875 + [[package]] 1876 + name = "rand_chacha" 1877 + version = "0.9.0" 1878 + source = "registry+https://github.com/rust-lang/crates.io-index" 1879 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1880 + dependencies = [ 1881 + "ppv-lite86", 1882 + "rand_core 0.9.0", 1883 + ] 1884 + 1885 + [[package]] 1886 + name = "rand_core" 1887 + version = "0.6.4" 1888 + source = "registry+https://github.com/rust-lang/crates.io-index" 1889 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1890 + dependencies = [ 1891 + "getrandom 0.2.15", 1892 + ] 1893 + 1894 + [[package]] 1895 + name = "rand_core" 1896 + version = "0.9.0" 1897 + source = "registry+https://github.com/rust-lang/crates.io-index" 1898 + checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" 1899 + dependencies = [ 1900 + "getrandom 0.3.1", 1901 + "zerocopy 0.8.17", 1902 + ] 1903 + 1904 + [[package]] 1905 + name = "redox_syscall" 1906 + version = "0.5.8" 1907 + source = "registry+https://github.com/rust-lang/crates.io-index" 1908 + checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 1909 + dependencies = [ 1910 + "bitflags", 1911 + ] 1912 + 1913 + [[package]] 1914 + name = "regex" 1915 + version = "1.11.1" 1916 + source = "registry+https://github.com/rust-lang/crates.io-index" 1917 + checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1918 + dependencies = [ 1919 + "aho-corasick", 1920 + "memchr", 1921 + "regex-automata 0.4.9", 1922 + "regex-syntax 0.8.5", 1923 + ] 1924 + 1925 + [[package]] 1926 + name = "regex-automata" 1927 + version = "0.1.10" 1928 + source = "registry+https://github.com/rust-lang/crates.io-index" 1929 + checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1930 + dependencies = [ 1931 + "regex-syntax 0.6.29", 1932 + ] 1933 + 1934 + [[package]] 1935 + name = "regex-automata" 1936 + version = "0.4.9" 1937 + source = "registry+https://github.com/rust-lang/crates.io-index" 1938 + checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1939 + dependencies = [ 1940 + "aho-corasick", 1941 + "memchr", 1942 + "regex-syntax 0.8.5", 1943 + ] 1944 + 1945 + [[package]] 1946 + name = "regex-syntax" 1947 + version = "0.6.29" 1948 + source = "registry+https://github.com/rust-lang/crates.io-index" 1949 + checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1950 + 1951 + [[package]] 1952 + name = "regex-syntax" 1953 + version = "0.8.5" 1954 + source = "registry+https://github.com/rust-lang/crates.io-index" 1955 + checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1956 + 1957 + [[package]] 1958 + name = "reqwest" 1959 + version = "0.12.12" 1960 + source = "registry+https://github.com/rust-lang/crates.io-index" 1961 + checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" 1962 + dependencies = [ 1963 + "base64 0.22.1", 1964 + "bytes", 1965 + "encoding_rs", 1966 + "futures-core", 1967 + "futures-util", 1968 + "h2", 1969 + "http", 1970 + "http-body", 1971 + "http-body-util", 1972 + "hyper", 1973 + "hyper-rustls", 1974 + "hyper-tls", 1975 + "hyper-util", 1976 + "ipnet", 1977 + "js-sys", 1978 + "log", 1979 + "mime", 1980 + "native-tls", 1981 + "once_cell", 1982 + "percent-encoding", 1983 + "pin-project-lite", 1984 + "rustls-pemfile", 1985 + "serde", 1986 + "serde_json", 1987 + "serde_urlencoded", 1988 + "sync_wrapper", 1989 + "system-configuration", 1990 + "tokio", 1991 + "tokio-native-tls", 1992 + "tower", 1993 + "tower-service", 1994 + "url", 1995 + "wasm-bindgen", 1996 + "wasm-bindgen-futures", 1997 + "web-sys", 1998 + "windows-registry", 1999 + ] 2000 + 2001 + [[package]] 2002 + name = "ring" 2003 + version = "0.17.8" 2004 + source = "registry+https://github.com/rust-lang/crates.io-index" 2005 + checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 2006 + dependencies = [ 2007 + "cc", 2008 + "cfg-if", 2009 + "getrandom 0.2.15", 2010 + "libc", 2011 + "spin", 2012 + "untrusted", 2013 + "windows-sys 0.52.0", 2014 + ] 2015 + 2016 + [[package]] 2017 + name = "rsa" 2018 + version = "0.9.7" 2019 + source = "registry+https://github.com/rust-lang/crates.io-index" 2020 + checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" 2021 + dependencies = [ 2022 + "const-oid", 2023 + "digest", 2024 + "num-bigint-dig", 2025 + "num-integer", 2026 + "num-traits", 2027 + "pkcs1", 2028 + "pkcs8", 2029 + "rand_core 0.6.4", 2030 + "signature", 2031 + "spki", 2032 + "subtle", 2033 + "zeroize", 2034 + ] 2035 + 2036 + [[package]] 2037 + name = "rustc-demangle" 2038 + version = "0.1.24" 2039 + source = "registry+https://github.com/rust-lang/crates.io-index" 2040 + checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 2041 + 2042 + [[package]] 2043 + name = "rustc_version" 2044 + version = "0.4.1" 2045 + source = "registry+https://github.com/rust-lang/crates.io-index" 2046 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2047 + dependencies = [ 2048 + "semver", 2049 + ] 2050 + 2051 + [[package]] 2052 + name = "rustix" 2053 + version = "0.38.44" 2054 + source = "registry+https://github.com/rust-lang/crates.io-index" 2055 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 2056 + dependencies = [ 2057 + "bitflags", 2058 + "errno", 2059 + "libc", 2060 + "linux-raw-sys", 2061 + "windows-sys 0.59.0", 2062 + ] 2063 + 2064 + [[package]] 2065 + name = "rustls" 2066 + version = "0.23.21" 2067 + source = "registry+https://github.com/rust-lang/crates.io-index" 2068 + checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" 2069 + dependencies = [ 2070 + "once_cell", 2071 + "ring", 2072 + "rustls-pki-types", 2073 + "rustls-webpki", 2074 + "subtle", 2075 + "zeroize", 2076 + ] 2077 + 2078 + [[package]] 2079 + name = "rustls-native-certs" 2080 + version = "0.8.1" 2081 + source = "registry+https://github.com/rust-lang/crates.io-index" 2082 + checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 2083 + dependencies = [ 2084 + "openssl-probe", 2085 + "rustls-pki-types", 2086 + "schannel", 2087 + "security-framework 3.2.0", 2088 + ] 2089 + 2090 + [[package]] 2091 + name = "rustls-pemfile" 2092 + version = "2.2.0" 2093 + source = "registry+https://github.com/rust-lang/crates.io-index" 2094 + checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 2095 + dependencies = [ 2096 + "rustls-pki-types", 2097 + ] 2098 + 2099 + [[package]] 2100 + name = "rustls-pki-types" 2101 + version = "1.11.0" 2102 + source = "registry+https://github.com/rust-lang/crates.io-index" 2103 + checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 2104 + 2105 + [[package]] 2106 + name = "rustls-webpki" 2107 + version = "0.102.8" 2108 + source = "registry+https://github.com/rust-lang/crates.io-index" 2109 + checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 2110 + dependencies = [ 2111 + "ring", 2112 + "rustls-pki-types", 2113 + "untrusted", 2114 + ] 2115 + 2116 + [[package]] 2117 + name = "rustversion" 2118 + version = "1.0.19" 2119 + source = "registry+https://github.com/rust-lang/crates.io-index" 2120 + checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 2121 + 2122 + [[package]] 2123 + name = "ryu" 2124 + version = "1.0.19" 2125 + source = "registry+https://github.com/rust-lang/crates.io-index" 2126 + checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 2127 + 2128 + [[package]] 2129 + name = "schannel" 2130 + version = "0.1.27" 2131 + source = "registry+https://github.com/rust-lang/crates.io-index" 2132 + checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 2133 + dependencies = [ 2134 + "windows-sys 0.59.0", 2135 + ] 2136 + 2137 + [[package]] 2138 + name = "scopeguard" 2139 + version = "1.2.0" 2140 + source = "registry+https://github.com/rust-lang/crates.io-index" 2141 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2142 + 2143 + [[package]] 2144 + name = "secp256k1" 2145 + version = "0.30.0" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" 2148 + dependencies = [ 2149 + "bitcoin_hashes", 2150 + "rand 0.8.5", 2151 + "secp256k1-sys", 2152 + "serde", 2153 + ] 2154 + 2155 + [[package]] 2156 + name = "secp256k1-sys" 2157 + version = "0.10.1" 2158 + source = "registry+https://github.com/rust-lang/crates.io-index" 2159 + checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" 2160 + dependencies = [ 2161 + "cc", 2162 + ] 2163 + 2164 + [[package]] 2165 + name = "security-framework" 2166 + version = "2.11.1" 2167 + source = "registry+https://github.com/rust-lang/crates.io-index" 2168 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2169 + dependencies = [ 2170 + "bitflags", 2171 + "core-foundation 0.9.4", 2172 + "core-foundation-sys", 2173 + "libc", 2174 + "security-framework-sys", 2175 + ] 2176 + 2177 + [[package]] 2178 + name = "security-framework" 2179 + version = "3.2.0" 2180 + source = "registry+https://github.com/rust-lang/crates.io-index" 2181 + checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 2182 + dependencies = [ 2183 + "bitflags", 2184 + "core-foundation 0.10.0", 2185 + "core-foundation-sys", 2186 + "libc", 2187 + "security-framework-sys", 2188 + ] 2189 + 2190 + [[package]] 2191 + name = "security-framework-sys" 2192 + version = "2.14.0" 2193 + source = "registry+https://github.com/rust-lang/crates.io-index" 2194 + checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 2195 + dependencies = [ 2196 + "core-foundation-sys", 2197 + "libc", 2198 + ] 2199 + 2200 + [[package]] 2201 + name = "semver" 2202 + version = "1.0.25" 2203 + source = "registry+https://github.com/rust-lang/crates.io-index" 2204 + checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" 2205 + 2206 + [[package]] 2207 + name = "serde" 2208 + version = "1.0.217" 2209 + source = "registry+https://github.com/rust-lang/crates.io-index" 2210 + checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 2211 + dependencies = [ 2212 + "serde_derive", 2213 + ] 2214 + 2215 + [[package]] 2216 + name = "serde_bytes" 2217 + version = "0.11.15" 2218 + source = "registry+https://github.com/rust-lang/crates.io-index" 2219 + checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" 2220 + dependencies = [ 2221 + "serde", 2222 + ] 2223 + 2224 + [[package]] 2225 + name = "serde_cbor" 2226 + version = "0.11.2" 2227 + source = "registry+https://github.com/rust-lang/crates.io-index" 2228 + checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" 2229 + dependencies = [ 2230 + "half", 2231 + "serde", 2232 + ] 2233 + 2234 + [[package]] 2235 + name = "serde_derive" 2236 + version = "1.0.217" 2237 + source = "registry+https://github.com/rust-lang/crates.io-index" 2238 + checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 2239 + dependencies = [ 2240 + "proc-macro2", 2241 + "quote", 2242 + "syn", 2243 + ] 2244 + 2245 + [[package]] 2246 + name = "serde_html_form" 2247 + version = "0.2.7" 2248 + source = "registry+https://github.com/rust-lang/crates.io-index" 2249 + checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" 2250 + dependencies = [ 2251 + "form_urlencoded", 2252 + "indexmap", 2253 + "itoa", 2254 + "ryu", 2255 + "serde", 2256 + ] 2257 + 2258 + [[package]] 2259 + name = "serde_json" 2260 + version = "1.0.138" 2261 + source = "registry+https://github.com/rust-lang/crates.io-index" 2262 + checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" 2263 + dependencies = [ 2264 + "itoa", 2265 + "memchr", 2266 + "ryu", 2267 + "serde", 2268 + ] 2269 + 2270 + [[package]] 2271 + name = "serde_path_to_error" 2272 + version = "0.1.16" 2273 + source = "registry+https://github.com/rust-lang/crates.io-index" 2274 + checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 2275 + dependencies = [ 2276 + "itoa", 2277 + "serde", 2278 + ] 2279 + 2280 + [[package]] 2281 + name = "serde_urlencoded" 2282 + version = "0.7.1" 2283 + source = "registry+https://github.com/rust-lang/crates.io-index" 2284 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2285 + dependencies = [ 2286 + "form_urlencoded", 2287 + "itoa", 2288 + "ryu", 2289 + "serde", 2290 + ] 2291 + 2292 + [[package]] 2293 + name = "sha1" 2294 + version = "0.10.6" 2295 + source = "registry+https://github.com/rust-lang/crates.io-index" 2296 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2297 + dependencies = [ 2298 + "cfg-if", 2299 + "cpufeatures", 2300 + "digest", 2301 + ] 2302 + 2303 + [[package]] 2304 + name = "sha2" 2305 + version = "0.10.8" 2306 + source = "registry+https://github.com/rust-lang/crates.io-index" 2307 + checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 2308 + dependencies = [ 2309 + "cfg-if", 2310 + "cpufeatures", 2311 + "digest", 2312 + ] 2313 + 2314 + [[package]] 2315 + name = "sharded-slab" 2316 + version = "0.1.7" 2317 + source = "registry+https://github.com/rust-lang/crates.io-index" 2318 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 2319 + dependencies = [ 2320 + "lazy_static", 2321 + ] 2322 + 2323 + [[package]] 2324 + name = "shlex" 2325 + version = "1.3.0" 2326 + source = "registry+https://github.com/rust-lang/crates.io-index" 2327 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2328 + 2329 + [[package]] 2330 + name = "signal-hook-registry" 2331 + version = "1.4.2" 2332 + source = "registry+https://github.com/rust-lang/crates.io-index" 2333 + checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 2334 + dependencies = [ 2335 + "libc", 2336 + ] 2337 + 2338 + [[package]] 2339 + name = "signature" 2340 + version = "2.2.0" 2341 + source = "registry+https://github.com/rust-lang/crates.io-index" 2342 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2343 + dependencies = [ 2344 + "digest", 2345 + "rand_core 0.6.4", 2346 + ] 2347 + 2348 + [[package]] 2349 + name = "slab" 2350 + version = "0.4.9" 2351 + source = "registry+https://github.com/rust-lang/crates.io-index" 2352 + checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 2353 + dependencies = [ 2354 + "autocfg", 2355 + ] 2356 + 2357 + [[package]] 2358 + name = "smallvec" 2359 + version = "1.13.2" 2360 + source = "registry+https://github.com/rust-lang/crates.io-index" 2361 + checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 2362 + dependencies = [ 2363 + "serde", 2364 + ] 2365 + 2366 + [[package]] 2367 + name = "socket2" 2368 + version = "0.5.8" 2369 + source = "registry+https://github.com/rust-lang/crates.io-index" 2370 + checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 2371 + dependencies = [ 2372 + "libc", 2373 + "windows-sys 0.52.0", 2374 + ] 2375 + 2376 + [[package]] 2377 + name = "spin" 2378 + version = "0.9.8" 2379 + source = "registry+https://github.com/rust-lang/crates.io-index" 2380 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2381 + dependencies = [ 2382 + "lock_api", 2383 + ] 2384 + 2385 + [[package]] 2386 + name = "spki" 2387 + version = "0.7.3" 2388 + source = "registry+https://github.com/rust-lang/crates.io-index" 2389 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 2390 + dependencies = [ 2391 + "base64ct", 2392 + "der", 2393 + ] 2394 + 2395 + [[package]] 2396 + name = "sqlx" 2397 + version = "0.8.3" 2398 + source = "registry+https://github.com/rust-lang/crates.io-index" 2399 + checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" 2400 + dependencies = [ 2401 + "sqlx-core", 2402 + "sqlx-macros", 2403 + "sqlx-mysql", 2404 + "sqlx-postgres", 2405 + "sqlx-sqlite", 2406 + ] 2407 + 2408 + [[package]] 2409 + name = "sqlx-core" 2410 + version = "0.8.3" 2411 + source = "registry+https://github.com/rust-lang/crates.io-index" 2412 + checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" 2413 + dependencies = [ 2414 + "bytes", 2415 + "chrono", 2416 + "crc", 2417 + "crossbeam-queue", 2418 + "either", 2419 + "event-listener", 2420 + "futures-core", 2421 + "futures-intrusive", 2422 + "futures-io", 2423 + "futures-util", 2424 + "hashbrown", 2425 + "hashlink", 2426 + "indexmap", 2427 + "log", 2428 + "memchr", 2429 + "once_cell", 2430 + "percent-encoding", 2431 + "rustls", 2432 + "rustls-native-certs", 2433 + "rustls-pemfile", 2434 + "serde", 2435 + "serde_json", 2436 + "sha2", 2437 + "smallvec", 2438 + "thiserror 2.0.11", 2439 + "tokio", 2440 + "tokio-stream", 2441 + "tracing", 2442 + "url", 2443 + ] 2444 + 2445 + [[package]] 2446 + name = "sqlx-macros" 2447 + version = "0.8.3" 2448 + source = "registry+https://github.com/rust-lang/crates.io-index" 2449 + checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" 2450 + dependencies = [ 2451 + "proc-macro2", 2452 + "quote", 2453 + "sqlx-core", 2454 + "sqlx-macros-core", 2455 + "syn", 2456 + ] 2457 + 2458 + [[package]] 2459 + name = "sqlx-macros-core" 2460 + version = "0.8.3" 2461 + source = "registry+https://github.com/rust-lang/crates.io-index" 2462 + checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" 2463 + dependencies = [ 2464 + "dotenvy", 2465 + "either", 2466 + "heck", 2467 + "hex", 2468 + "once_cell", 2469 + "proc-macro2", 2470 + "quote", 2471 + "serde", 2472 + "serde_json", 2473 + "sha2", 2474 + "sqlx-core", 2475 + "sqlx-mysql", 2476 + "sqlx-postgres", 2477 + "sqlx-sqlite", 2478 + "syn", 2479 + "tempfile", 2480 + "tokio", 2481 + "url", 2482 + ] 2483 + 2484 + [[package]] 2485 + name = "sqlx-mysql" 2486 + version = "0.8.3" 2487 + source = "registry+https://github.com/rust-lang/crates.io-index" 2488 + checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" 2489 + dependencies = [ 2490 + "atoi", 2491 + "base64 0.22.1", 2492 + "bitflags", 2493 + "byteorder", 2494 + "bytes", 2495 + "chrono", 2496 + "crc", 2497 + "digest", 2498 + "dotenvy", 2499 + "either", 2500 + "futures-channel", 2501 + "futures-core", 2502 + "futures-io", 2503 + "futures-util", 2504 + "generic-array", 2505 + "hex", 2506 + "hkdf", 2507 + "hmac", 2508 + "itoa", 2509 + "log", 2510 + "md-5", 2511 + "memchr", 2512 + "once_cell", 2513 + "percent-encoding", 2514 + "rand 0.8.5", 2515 + "rsa", 2516 + "serde", 2517 + "sha1", 2518 + "sha2", 2519 + "smallvec", 2520 + "sqlx-core", 2521 + "stringprep", 2522 + "thiserror 2.0.11", 2523 + "tracing", 2524 + "whoami", 2525 + ] 2526 + 2527 + [[package]] 2528 + name = "sqlx-postgres" 2529 + version = "0.8.3" 2530 + source = "registry+https://github.com/rust-lang/crates.io-index" 2531 + checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" 2532 + dependencies = [ 2533 + "atoi", 2534 + "base64 0.22.1", 2535 + "bitflags", 2536 + "byteorder", 2537 + "chrono", 2538 + "crc", 2539 + "dotenvy", 2540 + "etcetera", 2541 + "futures-channel", 2542 + "futures-core", 2543 + "futures-util", 2544 + "hex", 2545 + "hkdf", 2546 + "hmac", 2547 + "home", 2548 + "itoa", 2549 + "log", 2550 + "md-5", 2551 + "memchr", 2552 + "once_cell", 2553 + "rand 0.8.5", 2554 + "serde", 2555 + "serde_json", 2556 + "sha2", 2557 + "smallvec", 2558 + "sqlx-core", 2559 + "stringprep", 2560 + "thiserror 2.0.11", 2561 + "tracing", 2562 + "whoami", 2563 + ] 2564 + 2565 + [[package]] 2566 + name = "sqlx-sqlite" 2567 + version = "0.8.3" 2568 + source = "registry+https://github.com/rust-lang/crates.io-index" 2569 + checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" 2570 + dependencies = [ 2571 + "atoi", 2572 + "chrono", 2573 + "flume", 2574 + "futures-channel", 2575 + "futures-core", 2576 + "futures-executor", 2577 + "futures-intrusive", 2578 + "futures-util", 2579 + "libsqlite3-sys", 2580 + "log", 2581 + "percent-encoding", 2582 + "serde", 2583 + "serde_urlencoded", 2584 + "sqlx-core", 2585 + "tracing", 2586 + "url", 2587 + ] 2588 + 2589 + [[package]] 2590 + name = "stable_deref_trait" 2591 + version = "1.2.0" 2592 + source = "registry+https://github.com/rust-lang/crates.io-index" 2593 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2594 + 2595 + [[package]] 2596 + name = "stringprep" 2597 + version = "0.1.5" 2598 + source = "registry+https://github.com/rust-lang/crates.io-index" 2599 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 2600 + dependencies = [ 2601 + "unicode-bidi", 2602 + "unicode-normalization", 2603 + "unicode-properties", 2604 + ] 2605 + 2606 + [[package]] 2607 + name = "subtle" 2608 + version = "2.6.1" 2609 + source = "registry+https://github.com/rust-lang/crates.io-index" 2610 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2611 + 2612 + [[package]] 2613 + name = "syn" 2614 + version = "2.0.96" 2615 + source = "registry+https://github.com/rust-lang/crates.io-index" 2616 + checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 2617 + dependencies = [ 2618 + "proc-macro2", 2619 + "quote", 2620 + "unicode-ident", 2621 + ] 2622 + 2623 + [[package]] 2624 + name = "sync_wrapper" 2625 + version = "1.0.2" 2626 + source = "registry+https://github.com/rust-lang/crates.io-index" 2627 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2628 + dependencies = [ 2629 + "futures-core", 2630 + ] 2631 + 2632 + [[package]] 2633 + name = "synstructure" 2634 + version = "0.13.1" 2635 + source = "registry+https://github.com/rust-lang/crates.io-index" 2636 + checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 2637 + dependencies = [ 2638 + "proc-macro2", 2639 + "quote", 2640 + "syn", 2641 + ] 2642 + 2643 + [[package]] 2644 + name = "system-configuration" 2645 + version = "0.6.1" 2646 + source = "registry+https://github.com/rust-lang/crates.io-index" 2647 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2648 + dependencies = [ 2649 + "bitflags", 2650 + "core-foundation 0.9.4", 2651 + "system-configuration-sys", 2652 + ] 2653 + 2654 + [[package]] 2655 + name = "system-configuration-sys" 2656 + version = "0.6.0" 2657 + source = "registry+https://github.com/rust-lang/crates.io-index" 2658 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2659 + dependencies = [ 2660 + "core-foundation-sys", 2661 + "libc", 2662 + ] 2663 + 2664 + [[package]] 2665 + name = "tempfile" 2666 + version = "3.16.0" 2667 + source = "registry+https://github.com/rust-lang/crates.io-index" 2668 + checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" 2669 + dependencies = [ 2670 + "cfg-if", 2671 + "fastrand", 2672 + "getrandom 0.3.1", 2673 + "once_cell", 2674 + "rustix", 2675 + "windows-sys 0.59.0", 2676 + ] 2677 + 2678 + [[package]] 2679 + name = "thiserror" 2680 + version = "1.0.69" 2681 + source = "registry+https://github.com/rust-lang/crates.io-index" 2682 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2683 + dependencies = [ 2684 + "thiserror-impl 1.0.69", 2685 + ] 2686 + 2687 + [[package]] 2688 + name = "thiserror" 2689 + version = "2.0.11" 2690 + source = "registry+https://github.com/rust-lang/crates.io-index" 2691 + checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 2692 + dependencies = [ 2693 + "thiserror-impl 2.0.11", 2694 + ] 2695 + 2696 + [[package]] 2697 + name = "thiserror-impl" 2698 + version = "1.0.69" 2699 + source = "registry+https://github.com/rust-lang/crates.io-index" 2700 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2701 + dependencies = [ 2702 + "proc-macro2", 2703 + "quote", 2704 + "syn", 2705 + ] 2706 + 2707 + [[package]] 2708 + name = "thiserror-impl" 2709 + version = "2.0.11" 2710 + source = "registry+https://github.com/rust-lang/crates.io-index" 2711 + checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 2712 + dependencies = [ 2713 + "proc-macro2", 2714 + "quote", 2715 + "syn", 2716 + ] 2717 + 2718 + [[package]] 2719 + name = "thread_local" 2720 + version = "1.1.8" 2721 + source = "registry+https://github.com/rust-lang/crates.io-index" 2722 + checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 2723 + dependencies = [ 2724 + "cfg-if", 2725 + "once_cell", 2726 + ] 2727 + 2728 + [[package]] 2729 + name = "tinystr" 2730 + version = "0.7.6" 2731 + source = "registry+https://github.com/rust-lang/crates.io-index" 2732 + checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 2733 + dependencies = [ 2734 + "displaydoc", 2735 + "zerovec", 2736 + ] 2737 + 2738 + [[package]] 2739 + name = "tinyvec" 2740 + version = "1.8.1" 2741 + source = "registry+https://github.com/rust-lang/crates.io-index" 2742 + checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" 2743 + dependencies = [ 2744 + "tinyvec_macros", 2745 + ] 2746 + 2747 + [[package]] 2748 + name = "tinyvec_macros" 2749 + version = "0.1.1" 2750 + source = "registry+https://github.com/rust-lang/crates.io-index" 2751 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2752 + 2753 + [[package]] 2754 + name = "tokio" 2755 + version = "1.43.0" 2756 + source = "registry+https://github.com/rust-lang/crates.io-index" 2757 + checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 2758 + dependencies = [ 2759 + "backtrace", 2760 + "bytes", 2761 + "libc", 2762 + "mio", 2763 + "parking_lot", 2764 + "pin-project-lite", 2765 + "signal-hook-registry", 2766 + "socket2", 2767 + "tokio-macros", 2768 + "windows-sys 0.52.0", 2769 + ] 2770 + 2771 + [[package]] 2772 + name = "tokio-macros" 2773 + version = "2.5.0" 2774 + source = "registry+https://github.com/rust-lang/crates.io-index" 2775 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2776 + dependencies = [ 2777 + "proc-macro2", 2778 + "quote", 2779 + "syn", 2780 + ] 2781 + 2782 + [[package]] 2783 + name = "tokio-native-tls" 2784 + version = "0.3.1" 2785 + source = "registry+https://github.com/rust-lang/crates.io-index" 2786 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2787 + dependencies = [ 2788 + "native-tls", 2789 + "tokio", 2790 + ] 2791 + 2792 + [[package]] 2793 + name = "tokio-rustls" 2794 + version = "0.26.1" 2795 + source = "registry+https://github.com/rust-lang/crates.io-index" 2796 + checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" 2797 + dependencies = [ 2798 + "rustls", 2799 + "tokio", 2800 + ] 2801 + 2802 + [[package]] 2803 + name = "tokio-stream" 2804 + version = "0.1.17" 2805 + source = "registry+https://github.com/rust-lang/crates.io-index" 2806 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 2807 + dependencies = [ 2808 + "futures-core", 2809 + "pin-project-lite", 2810 + "tokio", 2811 + ] 2812 + 2813 + [[package]] 2814 + name = "tokio-tungstenite" 2815 + version = "0.24.0" 2816 + source = "registry+https://github.com/rust-lang/crates.io-index" 2817 + checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" 2818 + dependencies = [ 2819 + "futures-util", 2820 + "log", 2821 + "native-tls", 2822 + "tokio", 2823 + "tokio-native-tls", 2824 + "tungstenite 0.24.0", 2825 + ] 2826 + 2827 + [[package]] 2828 + name = "tokio-tungstenite" 2829 + version = "0.26.1" 2830 + source = "registry+https://github.com/rust-lang/crates.io-index" 2831 + checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a" 2832 + dependencies = [ 2833 + "futures-util", 2834 + "log", 2835 + "tokio", 2836 + "tungstenite 0.26.1", 2837 + ] 2838 + 2839 + [[package]] 2840 + name = "tokio-util" 2841 + version = "0.7.13" 2842 + source = "registry+https://github.com/rust-lang/crates.io-index" 2843 + checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 2844 + dependencies = [ 2845 + "bytes", 2846 + "futures-core", 2847 + "futures-sink", 2848 + "pin-project-lite", 2849 + "tokio", 2850 + ] 2851 + 2852 + [[package]] 2853 + name = "toml" 2854 + version = "0.5.11" 2855 + source = "registry+https://github.com/rust-lang/crates.io-index" 2856 + checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 2857 + dependencies = [ 2858 + "serde", 2859 + ] 2860 + 2861 + [[package]] 2862 + name = "tower" 2863 + version = "0.5.2" 2864 + source = "registry+https://github.com/rust-lang/crates.io-index" 2865 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2866 + dependencies = [ 2867 + "futures-core", 2868 + "futures-util", 2869 + "pin-project-lite", 2870 + "sync_wrapper", 2871 + "tokio", 2872 + "tower-layer", 2873 + "tower-service", 2874 + "tracing", 2875 + ] 2876 + 2877 + [[package]] 2878 + name = "tower-http" 2879 + version = "0.6.2" 2880 + source = "registry+https://github.com/rust-lang/crates.io-index" 2881 + checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" 2882 + dependencies = [ 2883 + "async-compression", 2884 + "bitflags", 2885 + "bytes", 2886 + "futures-core", 2887 + "futures-util", 2888 + "http", 2889 + "http-body", 2890 + "http-body-util", 2891 + "http-range-header", 2892 + "httpdate", 2893 + "mime", 2894 + "mime_guess", 2895 + "percent-encoding", 2896 + "pin-project-lite", 2897 + "tokio", 2898 + "tokio-util", 2899 + "tower-layer", 2900 + "tower-service", 2901 + "tracing", 2902 + ] 2903 + 2904 + [[package]] 2905 + name = "tower-layer" 2906 + version = "0.3.3" 2907 + source = "registry+https://github.com/rust-lang/crates.io-index" 2908 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2909 + 2910 + [[package]] 2911 + name = "tower-service" 2912 + version = "0.3.3" 2913 + source = "registry+https://github.com/rust-lang/crates.io-index" 2914 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2915 + 2916 + [[package]] 2917 + name = "tracing" 2918 + version = "0.1.41" 2919 + source = "registry+https://github.com/rust-lang/crates.io-index" 2920 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2921 + dependencies = [ 2922 + "log", 2923 + "pin-project-lite", 2924 + "tracing-attributes", 2925 + "tracing-core", 2926 + ] 2927 + 2928 + [[package]] 2929 + name = "tracing-attributes" 2930 + version = "0.1.28" 2931 + source = "registry+https://github.com/rust-lang/crates.io-index" 2932 + checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 2933 + dependencies = [ 2934 + "proc-macro2", 2935 + "quote", 2936 + "syn", 2937 + ] 2938 + 2939 + [[package]] 2940 + name = "tracing-core" 2941 + version = "0.1.33" 2942 + source = "registry+https://github.com/rust-lang/crates.io-index" 2943 + checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 2944 + dependencies = [ 2945 + "once_cell", 2946 + "valuable", 2947 + ] 2948 + 2949 + [[package]] 2950 + name = "tracing-log" 2951 + version = "0.2.0" 2952 + source = "registry+https://github.com/rust-lang/crates.io-index" 2953 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2954 + dependencies = [ 2955 + "log", 2956 + "once_cell", 2957 + "tracing-core", 2958 + ] 2959 + 2960 + [[package]] 2961 + name = "tracing-subscriber" 2962 + version = "0.3.19" 2963 + source = "registry+https://github.com/rust-lang/crates.io-index" 2964 + checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 2965 + dependencies = [ 2966 + "matchers", 2967 + "nu-ansi-term", 2968 + "once_cell", 2969 + "regex", 2970 + "sharded-slab", 2971 + "smallvec", 2972 + "thread_local", 2973 + "tracing", 2974 + "tracing-core", 2975 + "tracing-log", 2976 + ] 2977 + 2978 + [[package]] 2979 + name = "trait-variant" 2980 + version = "0.1.2" 2981 + source = "registry+https://github.com/rust-lang/crates.io-index" 2982 + checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 2983 + dependencies = [ 2984 + "proc-macro2", 2985 + "quote", 2986 + "syn", 2987 + ] 2988 + 2989 + [[package]] 2990 + name = "try-lock" 2991 + version = "0.2.5" 2992 + source = "registry+https://github.com/rust-lang/crates.io-index" 2993 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2994 + 2995 + [[package]] 2996 + name = "tungstenite" 2997 + version = "0.24.0" 2998 + source = "registry+https://github.com/rust-lang/crates.io-index" 2999 + checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" 3000 + dependencies = [ 3001 + "byteorder", 3002 + "bytes", 3003 + "data-encoding", 3004 + "http", 3005 + "httparse", 3006 + "log", 3007 + "native-tls", 3008 + "rand 0.8.5", 3009 + "sha1", 3010 + "thiserror 1.0.69", 3011 + "url", 3012 + "utf-8", 3013 + ] 3014 + 3015 + [[package]] 3016 + name = "tungstenite" 3017 + version = "0.26.1" 3018 + source = "registry+https://github.com/rust-lang/crates.io-index" 3019 + checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" 3020 + dependencies = [ 3021 + "byteorder", 3022 + "bytes", 3023 + "data-encoding", 3024 + "http", 3025 + "httparse", 3026 + "log", 3027 + "rand 0.8.5", 3028 + "sha1", 3029 + "thiserror 2.0.11", 3030 + "utf-8", 3031 + ] 3032 + 3033 + [[package]] 3034 + name = "typenum" 3035 + version = "1.17.0" 3036 + source = "registry+https://github.com/rust-lang/crates.io-index" 3037 + checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 3038 + 3039 + [[package]] 3040 + name = "unicase" 3041 + version = "2.8.1" 3042 + source = "registry+https://github.com/rust-lang/crates.io-index" 3043 + checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 3044 + 3045 + [[package]] 3046 + name = "unicode-bidi" 3047 + version = "0.3.18" 3048 + source = "registry+https://github.com/rust-lang/crates.io-index" 3049 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 3050 + 3051 + [[package]] 3052 + name = "unicode-ident" 3053 + version = "1.0.16" 3054 + source = "registry+https://github.com/rust-lang/crates.io-index" 3055 + checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 3056 + 3057 + [[package]] 3058 + name = "unicode-normalization" 3059 + version = "0.1.24" 3060 + source = "registry+https://github.com/rust-lang/crates.io-index" 3061 + checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 3062 + dependencies = [ 3063 + "tinyvec", 3064 + ] 3065 + 3066 + [[package]] 3067 + name = "unicode-properties" 3068 + version = "0.1.3" 3069 + source = "registry+https://github.com/rust-lang/crates.io-index" 3070 + checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 3071 + 3072 + [[package]] 3073 + name = "unsigned-varint" 3074 + version = "0.8.0" 3075 + source = "registry+https://github.com/rust-lang/crates.io-index" 3076 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 3077 + 3078 + [[package]] 3079 + name = "untrusted" 3080 + version = "0.9.0" 3081 + source = "registry+https://github.com/rust-lang/crates.io-index" 3082 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 3083 + 3084 + [[package]] 3085 + name = "url" 3086 + version = "2.5.4" 3087 + source = "registry+https://github.com/rust-lang/crates.io-index" 3088 + checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 3089 + dependencies = [ 3090 + "form_urlencoded", 3091 + "idna", 3092 + "percent-encoding", 3093 + ] 3094 + 3095 + [[package]] 3096 + name = "utf-8" 3097 + version = "0.7.6" 3098 + source = "registry+https://github.com/rust-lang/crates.io-index" 3099 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 3100 + 3101 + [[package]] 3102 + name = "utf16_iter" 3103 + version = "1.0.5" 3104 + source = "registry+https://github.com/rust-lang/crates.io-index" 3105 + checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 3106 + 3107 + [[package]] 3108 + name = "utf8_iter" 3109 + version = "1.0.4" 3110 + source = "registry+https://github.com/rust-lang/crates.io-index" 3111 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 3112 + 3113 + [[package]] 3114 + name = "valuable" 3115 + version = "0.1.1" 3116 + source = "registry+https://github.com/rust-lang/crates.io-index" 3117 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 3118 + 3119 + [[package]] 3120 + name = "vcpkg" 3121 + version = "0.2.15" 3122 + source = "registry+https://github.com/rust-lang/crates.io-index" 3123 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 3124 + 3125 + [[package]] 3126 + name = "version_check" 3127 + version = "0.9.5" 3128 + source = "registry+https://github.com/rust-lang/crates.io-index" 3129 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3130 + 3131 + [[package]] 3132 + name = "vswhom" 3133 + version = "0.1.0" 3134 + source = "registry+https://github.com/rust-lang/crates.io-index" 3135 + checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" 3136 + dependencies = [ 3137 + "libc", 3138 + "vswhom-sys", 3139 + ] 3140 + 3141 + [[package]] 3142 + name = "vswhom-sys" 3143 + version = "0.1.2" 3144 + source = "registry+https://github.com/rust-lang/crates.io-index" 3145 + checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" 3146 + dependencies = [ 3147 + "cc", 3148 + "libc", 3149 + ] 3150 + 3151 + [[package]] 3152 + name = "want" 3153 + version = "0.3.1" 3154 + source = "registry+https://github.com/rust-lang/crates.io-index" 3155 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 3156 + dependencies = [ 3157 + "try-lock", 3158 + ] 3159 + 3160 + [[package]] 3161 + name = "wasi" 3162 + version = "0.11.0+wasi-snapshot-preview1" 3163 + source = "registry+https://github.com/rust-lang/crates.io-index" 3164 + checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 3165 + 3166 + [[package]] 3167 + name = "wasi" 3168 + version = "0.13.3+wasi-0.2.2" 3169 + source = "registry+https://github.com/rust-lang/crates.io-index" 3170 + checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 3171 + dependencies = [ 3172 + "wit-bindgen-rt", 3173 + ] 3174 + 3175 + [[package]] 3176 + name = "wasite" 3177 + version = "0.1.0" 3178 + source = "registry+https://github.com/rust-lang/crates.io-index" 3179 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 3180 + 3181 + [[package]] 3182 + name = "wasm-bindgen" 3183 + version = "0.2.100" 3184 + source = "registry+https://github.com/rust-lang/crates.io-index" 3185 + checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 3186 + dependencies = [ 3187 + "cfg-if", 3188 + "once_cell", 3189 + "rustversion", 3190 + "wasm-bindgen-macro", 3191 + ] 3192 + 3193 + [[package]] 3194 + name = "wasm-bindgen-backend" 3195 + version = "0.2.100" 3196 + source = "registry+https://github.com/rust-lang/crates.io-index" 3197 + checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 3198 + dependencies = [ 3199 + "bumpalo", 3200 + "log", 3201 + "proc-macro2", 3202 + "quote", 3203 + "syn", 3204 + "wasm-bindgen-shared", 3205 + ] 3206 + 3207 + [[package]] 3208 + name = "wasm-bindgen-futures" 3209 + version = "0.4.50" 3210 + source = "registry+https://github.com/rust-lang/crates.io-index" 3211 + checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 3212 + dependencies = [ 3213 + "cfg-if", 3214 + "js-sys", 3215 + "once_cell", 3216 + "wasm-bindgen", 3217 + "web-sys", 3218 + ] 3219 + 3220 + [[package]] 3221 + name = "wasm-bindgen-macro" 3222 + version = "0.2.100" 3223 + source = "registry+https://github.com/rust-lang/crates.io-index" 3224 + checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 3225 + dependencies = [ 3226 + "quote", 3227 + "wasm-bindgen-macro-support", 3228 + ] 3229 + 3230 + [[package]] 3231 + name = "wasm-bindgen-macro-support" 3232 + version = "0.2.100" 3233 + source = "registry+https://github.com/rust-lang/crates.io-index" 3234 + checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 3235 + dependencies = [ 3236 + "proc-macro2", 3237 + "quote", 3238 + "syn", 3239 + "wasm-bindgen-backend", 3240 + "wasm-bindgen-shared", 3241 + ] 3242 + 3243 + [[package]] 3244 + name = "wasm-bindgen-shared" 3245 + version = "0.2.100" 3246 + source = "registry+https://github.com/rust-lang/crates.io-index" 3247 + checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 3248 + dependencies = [ 3249 + "unicode-ident", 3250 + ] 3251 + 3252 + [[package]] 3253 + name = "web-sys" 3254 + version = "0.3.77" 3255 + source = "registry+https://github.com/rust-lang/crates.io-index" 3256 + checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 3257 + dependencies = [ 3258 + "js-sys", 3259 + "wasm-bindgen", 3260 + ] 3261 + 3262 + [[package]] 3263 + name = "whoami" 3264 + version = "1.5.2" 3265 + source = "registry+https://github.com/rust-lang/crates.io-index" 3266 + checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 3267 + dependencies = [ 3268 + "redox_syscall", 3269 + "wasite", 3270 + ] 3271 + 3272 + [[package]] 3273 + name = "winapi" 3274 + version = "0.3.9" 3275 + source = "registry+https://github.com/rust-lang/crates.io-index" 3276 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 3277 + dependencies = [ 3278 + "winapi-i686-pc-windows-gnu", 3279 + "winapi-x86_64-pc-windows-gnu", 3280 + ] 3281 + 3282 + [[package]] 3283 + name = "winapi-i686-pc-windows-gnu" 3284 + version = "0.4.0" 3285 + source = "registry+https://github.com/rust-lang/crates.io-index" 3286 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3287 + 3288 + [[package]] 3289 + name = "winapi-x86_64-pc-windows-gnu" 3290 + version = "0.4.0" 3291 + source = "registry+https://github.com/rust-lang/crates.io-index" 3292 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 3293 + 3294 + [[package]] 3295 + name = "windows-core" 3296 + version = "0.52.0" 3297 + source = "registry+https://github.com/rust-lang/crates.io-index" 3298 + checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 3299 + dependencies = [ 3300 + "windows-targets 0.52.6", 3301 + ] 3302 + 3303 + [[package]] 3304 + name = "windows-registry" 3305 + version = "0.2.0" 3306 + source = "registry+https://github.com/rust-lang/crates.io-index" 3307 + checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 3308 + dependencies = [ 3309 + "windows-result", 3310 + "windows-strings", 3311 + "windows-targets 0.52.6", 3312 + ] 3313 + 3314 + [[package]] 3315 + name = "windows-result" 3316 + version = "0.2.0" 3317 + source = "registry+https://github.com/rust-lang/crates.io-index" 3318 + checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 3319 + dependencies = [ 3320 + "windows-targets 0.52.6", 3321 + ] 3322 + 3323 + [[package]] 3324 + name = "windows-strings" 3325 + version = "0.1.0" 3326 + source = "registry+https://github.com/rust-lang/crates.io-index" 3327 + checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 3328 + dependencies = [ 3329 + "windows-result", 3330 + "windows-targets 0.52.6", 3331 + ] 3332 + 3333 + [[package]] 3334 + name = "windows-sys" 3335 + version = "0.48.0" 3336 + source = "registry+https://github.com/rust-lang/crates.io-index" 3337 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3338 + dependencies = [ 3339 + "windows-targets 0.48.5", 3340 + ] 3341 + 3342 + [[package]] 3343 + name = "windows-sys" 3344 + version = "0.52.0" 3345 + source = "registry+https://github.com/rust-lang/crates.io-index" 3346 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3347 + dependencies = [ 3348 + "windows-targets 0.52.6", 3349 + ] 3350 + 3351 + [[package]] 3352 + name = "windows-sys" 3353 + version = "0.59.0" 3354 + source = "registry+https://github.com/rust-lang/crates.io-index" 3355 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3356 + dependencies = [ 3357 + "windows-targets 0.52.6", 3358 + ] 3359 + 3360 + [[package]] 3361 + name = "windows-targets" 3362 + version = "0.48.5" 3363 + source = "registry+https://github.com/rust-lang/crates.io-index" 3364 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3365 + dependencies = [ 3366 + "windows_aarch64_gnullvm 0.48.5", 3367 + "windows_aarch64_msvc 0.48.5", 3368 + "windows_i686_gnu 0.48.5", 3369 + "windows_i686_msvc 0.48.5", 3370 + "windows_x86_64_gnu 0.48.5", 3371 + "windows_x86_64_gnullvm 0.48.5", 3372 + "windows_x86_64_msvc 0.48.5", 3373 + ] 3374 + 3375 + [[package]] 3376 + name = "windows-targets" 3377 + version = "0.52.6" 3378 + source = "registry+https://github.com/rust-lang/crates.io-index" 3379 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3380 + dependencies = [ 3381 + "windows_aarch64_gnullvm 0.52.6", 3382 + "windows_aarch64_msvc 0.52.6", 3383 + "windows_i686_gnu 0.52.6", 3384 + "windows_i686_gnullvm", 3385 + "windows_i686_msvc 0.52.6", 3386 + "windows_x86_64_gnu 0.52.6", 3387 + "windows_x86_64_gnullvm 0.52.6", 3388 + "windows_x86_64_msvc 0.52.6", 3389 + ] 3390 + 3391 + [[package]] 3392 + name = "windows_aarch64_gnullvm" 3393 + version = "0.48.5" 3394 + source = "registry+https://github.com/rust-lang/crates.io-index" 3395 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3396 + 3397 + [[package]] 3398 + name = "windows_aarch64_gnullvm" 3399 + version = "0.52.6" 3400 + source = "registry+https://github.com/rust-lang/crates.io-index" 3401 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3402 + 3403 + [[package]] 3404 + name = "windows_aarch64_msvc" 3405 + version = "0.48.5" 3406 + source = "registry+https://github.com/rust-lang/crates.io-index" 3407 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3408 + 3409 + [[package]] 3410 + name = "windows_aarch64_msvc" 3411 + version = "0.52.6" 3412 + source = "registry+https://github.com/rust-lang/crates.io-index" 3413 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3414 + 3415 + [[package]] 3416 + name = "windows_i686_gnu" 3417 + version = "0.48.5" 3418 + source = "registry+https://github.com/rust-lang/crates.io-index" 3419 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3420 + 3421 + [[package]] 3422 + name = "windows_i686_gnu" 3423 + version = "0.52.6" 3424 + source = "registry+https://github.com/rust-lang/crates.io-index" 3425 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3426 + 3427 + [[package]] 3428 + name = "windows_i686_gnullvm" 3429 + version = "0.52.6" 3430 + source = "registry+https://github.com/rust-lang/crates.io-index" 3431 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3432 + 3433 + [[package]] 3434 + name = "windows_i686_msvc" 3435 + version = "0.48.5" 3436 + source = "registry+https://github.com/rust-lang/crates.io-index" 3437 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3438 + 3439 + [[package]] 3440 + name = "windows_i686_msvc" 3441 + version = "0.52.6" 3442 + source = "registry+https://github.com/rust-lang/crates.io-index" 3443 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3444 + 3445 + [[package]] 3446 + name = "windows_x86_64_gnu" 3447 + version = "0.48.5" 3448 + source = "registry+https://github.com/rust-lang/crates.io-index" 3449 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3450 + 3451 + [[package]] 3452 + name = "windows_x86_64_gnu" 3453 + version = "0.52.6" 3454 + source = "registry+https://github.com/rust-lang/crates.io-index" 3455 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3456 + 3457 + [[package]] 3458 + name = "windows_x86_64_gnullvm" 3459 + version = "0.48.5" 3460 + source = "registry+https://github.com/rust-lang/crates.io-index" 3461 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3462 + 3463 + [[package]] 3464 + name = "windows_x86_64_gnullvm" 3465 + version = "0.52.6" 3466 + source = "registry+https://github.com/rust-lang/crates.io-index" 3467 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3468 + 3469 + [[package]] 3470 + name = "windows_x86_64_msvc" 3471 + version = "0.48.5" 3472 + source = "registry+https://github.com/rust-lang/crates.io-index" 3473 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3474 + 3475 + [[package]] 3476 + name = "windows_x86_64_msvc" 3477 + version = "0.52.6" 3478 + source = "registry+https://github.com/rust-lang/crates.io-index" 3479 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3480 + 3481 + [[package]] 3482 + name = "winreg" 3483 + version = "0.10.1" 3484 + source = "registry+https://github.com/rust-lang/crates.io-index" 3485 + checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 3486 + dependencies = [ 3487 + "winapi", 3488 + ] 3489 + 3490 + [[package]] 3491 + name = "wit-bindgen-rt" 3492 + version = "0.33.0" 3493 + source = "registry+https://github.com/rust-lang/crates.io-index" 3494 + checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 3495 + dependencies = [ 3496 + "bitflags", 3497 + ] 3498 + 3499 + [[package]] 3500 + name = "write16" 3501 + version = "1.0.0" 3502 + source = "registry+https://github.com/rust-lang/crates.io-index" 3503 + checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 3504 + 3505 + [[package]] 3506 + name = "writeable" 3507 + version = "0.5.5" 3508 + source = "registry+https://github.com/rust-lang/crates.io-index" 3509 + checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 3510 + 3511 + [[package]] 3512 + name = "yoke" 3513 + version = "0.7.5" 3514 + source = "registry+https://github.com/rust-lang/crates.io-index" 3515 + checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 3516 + dependencies = [ 3517 + "serde", 3518 + "stable_deref_trait", 3519 + "yoke-derive", 3520 + "zerofrom", 3521 + ] 3522 + 3523 + [[package]] 3524 + name = "yoke-derive" 3525 + version = "0.7.5" 3526 + source = "registry+https://github.com/rust-lang/crates.io-index" 3527 + checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 3528 + dependencies = [ 3529 + "proc-macro2", 3530 + "quote", 3531 + "syn", 3532 + "synstructure", 3533 + ] 3534 + 3535 + [[package]] 3536 + name = "zerocopy" 3537 + version = "0.7.35" 3538 + source = "registry+https://github.com/rust-lang/crates.io-index" 3539 + checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 3540 + dependencies = [ 3541 + "byteorder", 3542 + "zerocopy-derive 0.7.35", 3543 + ] 3544 + 3545 + [[package]] 3546 + name = "zerocopy" 3547 + version = "0.8.17" 3548 + source = "registry+https://github.com/rust-lang/crates.io-index" 3549 + checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" 3550 + dependencies = [ 3551 + "zerocopy-derive 0.8.17", 3552 + ] 3553 + 3554 + [[package]] 3555 + name = "zerocopy-derive" 3556 + version = "0.7.35" 3557 + source = "registry+https://github.com/rust-lang/crates.io-index" 3558 + checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 3559 + dependencies = [ 3560 + "proc-macro2", 3561 + "quote", 3562 + "syn", 3563 + ] 3564 + 3565 + [[package]] 3566 + name = "zerocopy-derive" 3567 + version = "0.8.17" 3568 + source = "registry+https://github.com/rust-lang/crates.io-index" 3569 + checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" 3570 + dependencies = [ 3571 + "proc-macro2", 3572 + "quote", 3573 + "syn", 3574 + ] 3575 + 3576 + [[package]] 3577 + name = "zerofrom" 3578 + version = "0.1.5" 3579 + source = "registry+https://github.com/rust-lang/crates.io-index" 3580 + checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 3581 + dependencies = [ 3582 + "zerofrom-derive", 3583 + ] 3584 + 3585 + [[package]] 3586 + name = "zerofrom-derive" 3587 + version = "0.1.5" 3588 + source = "registry+https://github.com/rust-lang/crates.io-index" 3589 + checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 3590 + dependencies = [ 3591 + "proc-macro2", 3592 + "quote", 3593 + "syn", 3594 + "synstructure", 3595 + ] 3596 + 3597 + [[package]] 3598 + name = "zeroize" 3599 + version = "1.8.1" 3600 + source = "registry+https://github.com/rust-lang/crates.io-index" 3601 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 3602 + 3603 + [[package]] 3604 + name = "zerovec" 3605 + version = "0.10.4" 3606 + source = "registry+https://github.com/rust-lang/crates.io-index" 3607 + checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 3608 + dependencies = [ 3609 + "yoke", 3610 + "zerofrom", 3611 + "zerovec-derive", 3612 + ] 3613 + 3614 + [[package]] 3615 + name = "zerovec-derive" 3616 + version = "0.10.3" 3617 + source = "registry+https://github.com/rust-lang/crates.io-index" 3618 + checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 3619 + dependencies = [ 3620 + "proc-macro2", 3621 + "quote", 3622 + "syn", 3623 + ] 3624 + 3625 + [[package]] 3626 + name = "zstd" 3627 + version = "0.13.2" 3628 + source = "registry+https://github.com/rust-lang/crates.io-index" 3629 + checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" 3630 + dependencies = [ 3631 + "zstd-safe", 3632 + ] 3633 + 3634 + [[package]] 3635 + name = "zstd-safe" 3636 + version = "7.2.1" 3637 + source = "registry+https://github.com/rust-lang/crates.io-index" 3638 + checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" 3639 + dependencies = [ 3640 + "zstd-sys", 3641 + ] 3642 + 3643 + [[package]] 3644 + name = "zstd-sys" 3645 + version = "2.0.13+zstd.1.5.6" 3646 + source = "registry+https://github.com/rust-lang/crates.io-index" 3647 + checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" 3648 + dependencies = [ 3649 + "cc", 3650 + "pkg-config", 3651 + ]
+164
Cargo.toml
··· 1 + [package] 2 + name = "atproto_teq" 3 + version = "0.1.0" 4 + edition = "2024" 5 + publish = false 6 + authors = ["Timothy Quilling <teqed@shatteredsky.net>"] 7 + exclude = ["dist", "build", "assets", "credits"] 8 + description = "My ATProtocol applications and tools." 9 + license = "MIT" 10 + readme = "README.md" 11 + repository = "~/_/Repos/atproto" 12 + keywords = ["bluesky", "atproto"] 13 + categories = ["bluesky", "atproto"] 14 + 15 + [profile.dev.package."*"] 16 + opt-level = 3 17 + 18 + [profile.dev] 19 + opt-level = 1 20 + 21 + [profile.release] 22 + opt-level = "s" # Slightly slows compile times, great improvements to file size and runtime performance. 23 + lto = "thin" # Do a second optimization pass over the entire program, including dependencies. 24 + codegen-units = 1 # Compile the entire crate as one unit. 25 + strip = "debuginfo" # Strip all debugging information from the binary to slightly reduce file size. 26 + 27 + [features] 28 + dev = [ 29 + ] 30 + 31 + [build-dependencies] 32 + embed-resource = "1" 33 + 34 + 35 + [lints.rust] 36 + ## Groups 37 + warnings = { level = "warn", priority = -1 } # All lints that are set to issue warnings 38 + deprecated-safe = { level = "warn", priority = -1 } # Lints for functions which were erroneously marked as safe in the past 39 + future-incompatible = { level = "warn", priority = -1 } # Lints that detect code that has future-compatibility problems 40 + keyword-idents = { level = "warn", priority = -1 } # Lints that detect identifiers which will be come keywords in later editions 41 + let-underscore = { level = "warn", priority = -1 } # Lints that detect wildcard let bindings that are likely to be invalid 42 + nonstandard-style = { level = "warn", priority = -1 } # Violation of standard naming conventions 43 + refining-impl-trait = { level = "warn", priority = -1 } # Detects refinement of impl Trait return types by trait implementations 44 + rust-2018-compatibility = { level = "warn", priority = -1 } # Lints used to transition code from the 2015 edition to 2018 45 + rust-2021-compatibility = { level = "warn", priority = -1 } # Lints used to transition code from the 2018 edition to 2021 46 + rust-2018-idioms = { level = "warn", priority = -1 } # Lints to nudge you toward idiomatic features of Rust 2018 47 + rust-2024-compatibility = { level = "warn", priority = -1 } # Lints used to transition code from the 2021 edition to 2024 48 + unused = { level = "warn", priority = -1 } # Lints that detect things being declared but not used, or excess syntax 49 + ## Individual 50 + ambiguous_negative_literals = "warn" # checks for cases that are confusing between a negative literal and a negation that's not part of the literal. 51 + closure_returning_async_block = "warn" # detects cases where users write a closure that returns an async block. # nightly 52 + ffi_unwind_calls = "warn" 53 + # fuzzy_provenance_casts = "warn" # unstable 54 + # lossy_provenance_casts = "warn" # unstable 55 + macro_use_extern_crate = "warn" 56 + meta_variable_misuse = "warn" 57 + missing_abi = "warn" 58 + missing_copy_implementations = "allow" # detects potentially-forgotten implementations of Copy for public types. 59 + missing_debug_implementations = "allow" # detects missing implementations of fmt::Debug for public types. 60 + missing_docs = "warn" 61 + # multiple_supertrait_upcastable = "warn" # unstable 62 + # must_not_suspend = "warn" # unstable 63 + non_ascii_idents = "warn" 64 + # non_exhaustive_omitted_patterns = "warn" # unstable 65 + redundant_imports = "warn" 66 + redundant_lifetimes = "warn" 67 + rust_2024_incompatible_pat = "warn" # nightly 68 + single_use_lifetimes = "warn" 69 + trivial_casts = "warn" 70 + trivial_numeric_casts = "warn" 71 + unit_bindings = "warn" 72 + unnameable_types = "warn" 73 + # unqualified_local_imports = "warn" # unstable 74 + unreachable_pub = "warn" 75 + unsafe_code = "forbid" 76 + unstable_features = "warn" 77 + # unused_crate_dependencies = "warn" 78 + unused_import_braces = "warn" 79 + unused_lifetimes = "warn" 80 + unused_qualifications = "warn" 81 + unused_results = "warn" 82 + variant_size_differences = "warn" 83 + elided_lifetimes_in_paths = "allow" 84 + # unstable-features = "allow" 85 + 86 + [lints.clippy] 87 + # Groups 88 + nursery = { level = "warn", priority = -1 } 89 + correctness = { level = "warn", priority = -1 } 90 + suspicious = { level = "warn", priority = -1 } 91 + complexity = { level = "warn", priority = -1 } 92 + perf = { level = "warn", priority = -1 } 93 + style = { level = "warn", priority = -1 } 94 + # pedantic = { level = "warn", priority = -1 } 95 + # restriction = { level = "warn", priority = -1 } 96 + cargo = { level = "warn", priority = -1 } 97 + # Temporary Allows 98 + single_call_fn = "allow" 99 + multiple_crate_versions = "allow" 100 + expect_used = "allow" 101 + # Style Allows 102 + implicit_return = "allow" 103 + self_named_module_files = "allow" 104 + else_if_without_else = "allow" 105 + std_instead_of_alloc = "allow" 106 + std_instead_of_core = "allow" 107 + blanket_clippy_restriction_lints = "allow" 108 + float_arithmetic = "allow" 109 + redundant_pub_crate = "allow" 110 + pub_with_shorthand = "allow" 111 + absolute_paths = "allow" 112 + module_name_repetitions = "allow" 113 + missing_trait_methods = "allow" 114 + separated_literal_suffix = "allow" 115 + exhaustive_structs = "allow" 116 + field_scoped_visibility_modifiers = "allow" 117 + allow_attributes_without_reason = "allow" 118 + # Warns 119 + missing_docs_in_private_items = "warn" 120 + use_self = "warn" 121 + str_to_string = "warn" 122 + print_stdout = "warn" 123 + unseparated_literal_suffix = "warn" 124 + unwrap_used = "warn" 125 + # Denys 126 + enum_glob_use = "deny" 127 + # expect_used = "deny" 128 + 129 + [dependencies] 130 + atrium-api = { version = "0.24.10", default-features = false, features = [ 131 + "namespace-appbsky", 132 + ] } 133 + axum = {version = "0.8.1", features = ["ws"]} 134 + axum-extra = {version = "0.10.0", features = ["typed-header"]} 135 + base64 = "0.22.1" 136 + bs58 = "0.5.1" 137 + bytes = "1.10.0" 138 + chrono = "0.4.39" 139 + # derive_more = "2.0.1" 140 + dotenvy = "0.15.7" 141 + futures = "0.3.31" 142 + headers = "0.4.0" 143 + hex = "0.4.3" 144 + hmac = "0.12.1" 145 + http = "1.2.0" 146 + jetstream-oxide = "0.1.1" 147 + num = "0.4.3" 148 + num-derive = "0.4.2" 149 + num-traits = "0.2.19" 150 + rand = "0.9.0" 151 + rand_core = { version = "0.6.4" } 152 + # nutype = "0.6.0" 153 + reqwest = { version = "0.12.12", features = ["json"] } 154 + secp256k1 = { version = "0.30.0", features = ["hashes", "rand", "serde"] } 155 + serde = { version = "1.0.217", features = ["derive"] } 156 + serde_bytes = "0.11.15" 157 + serde_cbor = "0.11.2" 158 + serde_json = "1.0.138" 159 + sha2 = "0.10.8" 160 + sqlx = { version = "0.8.3", features= ["chrono", "json", "macros", "sqlite", "runtime-tokio", "tls-rustls-ring-native-roots"] } 161 + tokio = { version = "1.43.0", features = ["full"] } 162 + tower-http = {version = "0.6.2", features = ["compression-full", "decompression-deflate", "fs", "trace"]} 163 + tracing = "0.1" 164 + tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+19
README.md
··· 1 + # ATProto - Applications & Tools 2 + 3 + Rust binaries for hosting and interacting with ATProtocol services. 4 + 5 + ## Implemented endpoints 6 + - [x] `WSS /xrpc/com.atproto.label.subscribeLabels` 7 + - [x] `GET /xrpc/com.atproto.label.queryLabels` 8 + - [x] `GET /xrpc/app.bsky.actor.getProfile` 9 + - [ ] `GET /xrpc/app.bsky.feed.getFeedSkeleton` (WIP) 10 + 11 + ## Setup 12 + 1. Copy `.env.example` to `.env` and fill in the required values. 13 + 2. Copy `dev.db` to `prod.db`. 14 + 3. Compile the project with `cargo build --release`. 15 + 4. Run desired binaries: 16 + - `./target/release/database` - Database services. 17 + - `./target/release/jetstream` - Watch jetstream. 18 + - `./target/release/negation` - Issue a negation. 19 + - `./target/release/webserve` - Serve routes.
dev.db

This is a binary file and will not be displayed.

+82
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-utils": { 4 + "inputs": { 5 + "systems": "systems" 6 + }, 7 + "locked": { 8 + "lastModified": 1731533236, 9 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 + "owner": "numtide", 11 + "repo": "flake-utils", 12 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 + "type": "github" 14 + }, 15 + "original": { 16 + "owner": "numtide", 17 + "repo": "flake-utils", 18 + "type": "github" 19 + } 20 + }, 21 + "nixpkgs": { 22 + "locked": { 23 + "lastModified": 1738142207, 24 + "narHash": "sha256-NGqpVVxNAHwIicXpgaVqJEJWeyqzoQJ9oc8lnK9+WC4=", 25 + "owner": "NixOS", 26 + "repo": "nixpkgs", 27 + "rev": "9d3ae807ebd2981d593cddd0080856873139aa40", 28 + "type": "github" 29 + }, 30 + "original": { 31 + "owner": "NixOS", 32 + "ref": "nixos-unstable", 33 + "repo": "nixpkgs", 34 + "type": "github" 35 + } 36 + }, 37 + "root": { 38 + "inputs": { 39 + "flake-utils": "flake-utils", 40 + "nixpkgs": "nixpkgs", 41 + "rust-overlay": "rust-overlay" 42 + } 43 + }, 44 + "rust-overlay": { 45 + "inputs": { 46 + "nixpkgs": [ 47 + "nixpkgs" 48 + ] 49 + }, 50 + "locked": { 51 + "lastModified": 1738290352, 52 + "narHash": "sha256-YKOHUmc0Clm4tMV8grnxYL4IIwtjTayoq/3nqk0QM7k=", 53 + "owner": "oxalica", 54 + "repo": "rust-overlay", 55 + "rev": "b031b584125d33d23a0182f91ddbaf3ab4880236", 56 + "type": "github" 57 + }, 58 + "original": { 59 + "owner": "oxalica", 60 + "repo": "rust-overlay", 61 + "type": "github" 62 + } 63 + }, 64 + "systems": { 65 + "locked": { 66 + "lastModified": 1681028828, 67 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 + "owner": "nix-systems", 69 + "repo": "default", 70 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 + "type": "github" 72 + }, 73 + "original": { 74 + "owner": "nix-systems", 75 + "repo": "default", 76 + "type": "github" 77 + } 78 + } 79 + }, 80 + "root": "root", 81 + "version": 7 82 + }
+63
flake.nix
··· 1 + { 2 + description = "Rust development environment and shell"; 3 + inputs = { 4 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 + flake-utils.url = "github:numtide/flake-utils"; 6 + rust-overlay = { 7 + url = "github:oxalica/rust-overlay"; 8 + inputs = { 9 + nixpkgs.follows = "nixpkgs"; 10 + flake-utils.follows = "flake-utils"; 11 + }; 12 + }; 13 + }; 14 + outputs = { self, nixpkgs, flake-utils, rust-overlay }: 15 + flake-utils.lib.eachDefaultSystem 16 + (system: 17 + let 18 + buildInputs = with pkgs; [ 19 + udev 20 + alsa-lib 21 + vulkan-loader 22 + xorg.libX11 23 + xorg.libXcursor 24 + xorg.libXi 25 + xorg.libXrandr # To use the x11 feature 26 + libxkbcommon 27 + wayland # To use the wayland feature 28 + openssl 29 + pkg-config 30 + gcc 31 + pkg-config 32 + rust 33 + bacon 34 + clippy 35 + trunk# For WASM development 36 + # tracy # Profiler 37 + sqlite 38 + sqlite-web 39 + ]; 40 + overlays = [ (import rust-overlay) ]; 41 + pkgs = import nixpkgs { 42 + inherit system overlays; 43 + }; 44 + rust = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { 45 + extensions = [ 46 + "rust-src" # for rust-analyzer 47 + "rust-analyzer" 48 + ]; 49 + targets = [ "wasm32-unknown-unknown" ]; 50 + }); 51 + nativeBuildInputs = with pkgs; [ rust pkg-config ]; 52 + in 53 + with pkgs; 54 + { 55 + devShells.default = mkShell { 56 + inherit buildInputs nativeBuildInputs; 57 + LD_LIBRARY_PATH = nixpkgs.legacyPackages.x86_64-linux.lib.makeLibraryPath buildInputs; 58 + RUST_BACKTRACE = 1; 59 + DATABASE_URL = "sqlite://prod.db"; 60 + }; 61 + } 62 + ); 63 + }
+580
labeler_service.json
··· 1 + { 2 + "$type": "app.bsky.labeler.service", 3 + "policies": { 4 + "labelValues": [ 5 + "joined-nov-a", 6 + "joined-dec-a", 7 + "joined-jan-b", 8 + "joined-feb-b", 9 + "joined-mar-b", 10 + "joined-apr-b", 11 + "joined-may-b", 12 + "joined-jun-b", 13 + "joined-jul-b", 14 + "joined-aug-b", 15 + "joined-sep-b", 16 + "joined-oct-b", 17 + "joined-nov-b", 18 + "joined-dec-b", 19 + "joined-jan-c", 20 + "joined-feb-c", 21 + "joined-mar-c", 22 + "joined-apr-c", 23 + "joined-may-c", 24 + "joined-jun-c", 25 + "joined-jul-c", 26 + "joined-aug-c", 27 + "joined-sep-c", 28 + "joined-oct-c", 29 + "joined-nov-c", 30 + "joined-dec-c", 31 + "joined-jan-d", 32 + "joined-feb-d", 33 + "joined-mar-d", 34 + "joined-apr-d", 35 + "joined-may-d", 36 + "joined-jun-d", 37 + "joined-jul-d", 38 + "joined-aug-d", 39 + "joined-sep-d", 40 + "joined-oct-d", 41 + "joined-nov-d", 42 + "joined-dec-d" 43 + ], 44 + "labelValueDefinitions": [ 45 + { 46 + "blurs": "none", 47 + "locales": [ 48 + { 49 + "lang": "en", 50 + "name": "Joined Nov ’22", 51 + "description": "This profile was created in November of 2022." 52 + } 53 + ], 54 + "severity": "inform", 55 + "adultOnly": false, 56 + "identifier": "joined-nov-a", 57 + "defaultSetting": "warn" 58 + }, 59 + { 60 + "blurs": "none", 61 + "locales": [ 62 + { 63 + "lang": "en", 64 + "name": "Joined Dec ’22", 65 + "description": "This profile was created in December of 2022." 66 + } 67 + ], 68 + "severity": "inform", 69 + "adultOnly": false, 70 + "identifier": "joined-dec-a", 71 + "defaultSetting": "warn" 72 + }, 73 + { 74 + "blurs": "none", 75 + "locales": [ 76 + { 77 + "lang": "en", 78 + "name": "Joined Jan ’23", 79 + "description": "This profile was created in January of 2023." 80 + } 81 + ], 82 + "severity": "inform", 83 + "adultOnly": false, 84 + "identifier": "joined-jan-b", 85 + "defaultSetting": "warn" 86 + }, 87 + { 88 + "blurs": "none", 89 + "locales": [ 90 + { 91 + "lang": "en", 92 + "name": "Joined Feb ’23", 93 + "description": "This profile was created in February of 2023." 94 + } 95 + ], 96 + "severity": "inform", 97 + "adultOnly": false, 98 + "identifier": "joined-feb-b", 99 + "defaultSetting": "warn" 100 + }, 101 + { 102 + "blurs": "none", 103 + "locales": [ 104 + { 105 + "lang": "en", 106 + "name": "Joined Mar ’23", 107 + "description": "This profile was created in March of 2023." 108 + } 109 + ], 110 + "severity": "inform", 111 + "adultOnly": false, 112 + "identifier": "joined-mar-b", 113 + "defaultSetting": "warn" 114 + }, 115 + { 116 + "blurs": "none", 117 + "locales": [ 118 + { 119 + "lang": "en", 120 + "name": "Joined Apr ’23", 121 + "description": "This profile was created in April of 2023." 122 + } 123 + ], 124 + "severity": "inform", 125 + "adultOnly": false, 126 + "identifier": "joined-apr-b", 127 + "defaultSetting": "warn" 128 + }, 129 + { 130 + "blurs": "none", 131 + "locales": [ 132 + { 133 + "lang": "en", 134 + "name": "Joined May ’23", 135 + "description": "This profile was created in May of 2023." 136 + } 137 + ], 138 + "severity": "inform", 139 + "adultOnly": false, 140 + "identifier": "joined-may-b", 141 + "defaultSetting": "warn" 142 + }, 143 + { 144 + "blurs": "none", 145 + "locales": [ 146 + { 147 + "lang": "en", 148 + "name": "Joined Jun ’23", 149 + "description": "This profile was created in June of 2023." 150 + } 151 + ], 152 + "severity": "inform", 153 + "adultOnly": false, 154 + "identifier": "joined-jun-b", 155 + "defaultSetting": "warn" 156 + }, 157 + { 158 + "blurs": "none", 159 + "locales": [ 160 + { 161 + "lang": "en", 162 + "name": "Joined Jul ’23", 163 + "description": "This profile was created in July of 2023." 164 + } 165 + ], 166 + "severity": "inform", 167 + "adultOnly": false, 168 + "identifier": "joined-jul-b", 169 + "defaultSetting": "warn" 170 + }, 171 + { 172 + "blurs": "none", 173 + "locales": [ 174 + { 175 + "lang": "en", 176 + "name": "Joined Aug ’23", 177 + "description": "This profile was created in August of 2023." 178 + } 179 + ], 180 + "severity": "inform", 181 + "adultOnly": false, 182 + "identifier": "joined-aug-b", 183 + "defaultSetting": "warn" 184 + }, 185 + { 186 + "blurs": "none", 187 + "locales": [ 188 + { 189 + "lang": "en", 190 + "name": "Joined Sep ’23", 191 + "description": "This profile was created in September of 2023." 192 + } 193 + ], 194 + "severity": "inform", 195 + "adultOnly": false, 196 + "identifier": "joined-sep-b", 197 + "defaultSetting": "warn" 198 + }, 199 + { 200 + "blurs": "none", 201 + "locales": [ 202 + { 203 + "lang": "en", 204 + "name": "Joined Oct ’23", 205 + "description": "This profile was created in October of 2023." 206 + } 207 + ], 208 + "severity": "inform", 209 + "adultOnly": false, 210 + "identifier": "joined-oct-b", 211 + "defaultSetting": "warn" 212 + }, 213 + { 214 + "blurs": "none", 215 + "locales": [ 216 + { 217 + "lang": "en", 218 + "name": "Joined Nov ’23", 219 + "description": "This profile was created in November of 2023." 220 + } 221 + ], 222 + "severity": "inform", 223 + "adultOnly": false, 224 + "identifier": "joined-nov-b", 225 + "defaultSetting": "warn" 226 + }, 227 + { 228 + "blurs": "none", 229 + "locales": [ 230 + { 231 + "lang": "en", 232 + "name": "Joined Dec ’23", 233 + "description": "This profile was created in December of 2023." 234 + } 235 + ], 236 + "severity": "inform", 237 + "adultOnly": false, 238 + "identifier": "joined-dec-b", 239 + "defaultSetting": "warn" 240 + }, 241 + { 242 + "blurs": "none", 243 + "locales": [ 244 + { 245 + "lang": "en", 246 + "name": "Joined Jan ’24", 247 + "description": "This profile was created in January of 2024." 248 + } 249 + ], 250 + "severity": "inform", 251 + "adultOnly": false, 252 + "identifier": "joined-jan-c", 253 + "defaultSetting": "warn" 254 + }, 255 + { 256 + "blurs": "none", 257 + "locales": [ 258 + { 259 + "lang": "en", 260 + "name": "Joined Feb ’24", 261 + "description": "This profile was created in February of 2024." 262 + } 263 + ], 264 + "severity": "inform", 265 + "adultOnly": false, 266 + "identifier": "joined-feb-c", 267 + "defaultSetting": "warn" 268 + }, 269 + { 270 + "blurs": "none", 271 + "locales": [ 272 + { 273 + "lang": "en", 274 + "name": "Joined Mar ’24", 275 + "description": "This profile was created in March of 2024." 276 + } 277 + ], 278 + "severity": "inform", 279 + "adultOnly": false, 280 + "identifier": "joined-mar-c", 281 + "defaultSetting": "warn" 282 + }, 283 + { 284 + "blurs": "none", 285 + "locales": [ 286 + { 287 + "lang": "en", 288 + "name": "Joined Apr ’24", 289 + "description": "This profile was created in April of 2024." 290 + } 291 + ], 292 + "severity": "inform", 293 + "adultOnly": false, 294 + "identifier": "joined-apr-c", 295 + "defaultSetting": "warn" 296 + }, 297 + { 298 + "blurs": "none", 299 + "locales": [ 300 + { 301 + "lang": "en", 302 + "name": "Joined May ’24", 303 + "description": "This profile was created in May of 2024." 304 + } 305 + ], 306 + "severity": "inform", 307 + "adultOnly": false, 308 + "identifier": "joined-may-c", 309 + "defaultSetting": "warn" 310 + }, 311 + { 312 + "blurs": "none", 313 + "locales": [ 314 + { 315 + "lang": "en", 316 + "name": "Joined Jun ’24", 317 + "description": "This profile was created in June of 2024." 318 + } 319 + ], 320 + "severity": "inform", 321 + "adultOnly": false, 322 + "identifier": "joined-jun-c", 323 + "defaultSetting": "warn" 324 + }, 325 + { 326 + "blurs": "none", 327 + "locales": [ 328 + { 329 + "lang": "en", 330 + "name": "Joined Jul ’24", 331 + "description": "This profile was created in July of 2024." 332 + } 333 + ], 334 + "severity": "inform", 335 + "adultOnly": false, 336 + "identifier": "joined-jul-c", 337 + "defaultSetting": "warn" 338 + }, 339 + { 340 + "blurs": "none", 341 + "locales": [ 342 + { 343 + "lang": "en", 344 + "name": "Joined Aug ’24", 345 + "description": "This profile was created in August of 2024." 346 + } 347 + ], 348 + "severity": "inform", 349 + "adultOnly": false, 350 + "identifier": "joined-aug-c", 351 + "defaultSetting": "warn" 352 + }, 353 + { 354 + "blurs": "none", 355 + "locales": [ 356 + { 357 + "lang": "en", 358 + "name": "Joined Sep ’24", 359 + "description": "This profile was created in September of 2024." 360 + } 361 + ], 362 + "severity": "inform", 363 + "adultOnly": false, 364 + "identifier": "joined-sep-c", 365 + "defaultSetting": "warn" 366 + }, 367 + { 368 + "blurs": "none", 369 + "locales": [ 370 + { 371 + "lang": "en", 372 + "name": "Joined Oct ’24", 373 + "description": "This profile was created in October of 2024." 374 + } 375 + ], 376 + "severity": "inform", 377 + "adultOnly": false, 378 + "identifier": "joined-oct-c", 379 + "defaultSetting": "warn" 380 + }, 381 + { 382 + "blurs": "none", 383 + "locales": [ 384 + { 385 + "lang": "en", 386 + "name": "Joined Nov ’24", 387 + "description": "This profile was created in November of 2024." 388 + } 389 + ], 390 + "severity": "inform", 391 + "adultOnly": false, 392 + "identifier": "joined-nov-c", 393 + "defaultSetting": "warn" 394 + }, 395 + { 396 + "blurs": "none", 397 + "locales": [ 398 + { 399 + "lang": "en", 400 + "name": "Joined Dec ’24", 401 + "description": "This profile was created in December of 2024." 402 + } 403 + ], 404 + "severity": "inform", 405 + "adultOnly": false, 406 + "identifier": "joined-dec-c", 407 + "defaultSetting": "warn" 408 + }, 409 + { 410 + "blurs": "none", 411 + "locales": [ 412 + { 413 + "lang": "en", 414 + "name": "Joined Jan ’25", 415 + "description": "This profile was created in January of 2025." 416 + } 417 + ], 418 + "severity": "inform", 419 + "adultOnly": false, 420 + "identifier": "joined-jan-d", 421 + "defaultSetting": "warn" 422 + }, 423 + { 424 + "blurs": "none", 425 + "locales": [ 426 + { 427 + "lang": "en", 428 + "name": "Joined Feb ’25", 429 + "description": "This profile was created in February of 2025." 430 + } 431 + ], 432 + "severity": "inform", 433 + "adultOnly": false, 434 + "identifier": "joined-feb-d", 435 + "defaultSetting": "warn" 436 + }, 437 + { 438 + "blurs": "none", 439 + "locales": [ 440 + { 441 + "lang": "en", 442 + "name": "Joined Mar ’25", 443 + "description": "This profile was created in March of 2025." 444 + } 445 + ], 446 + "severity": "inform", 447 + "adultOnly": false, 448 + "identifier": "joined-mar-d", 449 + "defaultSetting": "warn" 450 + }, 451 + { 452 + "blurs": "none", 453 + "locales": [ 454 + { 455 + "lang": "en", 456 + "name": "Joined Apr ’25", 457 + "description": "This profile was created in April of 2025." 458 + } 459 + ], 460 + "severity": "inform", 461 + "adultOnly": false, 462 + "identifier": "joined-apr-d", 463 + "defaultSetting": "warn" 464 + }, 465 + { 466 + "blurs": "none", 467 + "locales": [ 468 + { 469 + "lang": "en", 470 + "name": "Joined May ’25", 471 + "description": "This profile was created in May of 2025." 472 + } 473 + ], 474 + "severity": "inform", 475 + "adultOnly": false, 476 + "identifier": "joined-may-d", 477 + "defaultSetting": "warn" 478 + }, 479 + { 480 + "blurs": "none", 481 + "locales": [ 482 + { 483 + "lang": "en", 484 + "name": "Joined Jun ’25", 485 + "description": "This profile was created in June of 2025." 486 + } 487 + ], 488 + "severity": "inform", 489 + "adultOnly": false, 490 + "identifier": "joined-jun-d", 491 + "defaultSetting": "warn" 492 + }, 493 + { 494 + "blurs": "none", 495 + "locales": [ 496 + { 497 + "lang": "en", 498 + "name": "Joined Jul ’25", 499 + "description": "This profile was created in July of 2025." 500 + } 501 + ], 502 + "severity": "inform", 503 + "adultOnly": false, 504 + "identifier": "joined-jul-d", 505 + "defaultSetting": "warn" 506 + }, 507 + { 508 + "blurs": "none", 509 + "locales": [ 510 + { 511 + "lang": "en", 512 + "name": "Joined Aug ’25", 513 + "description": "This profile was created in August of 2025." 514 + } 515 + ], 516 + "severity": "inform", 517 + "adultOnly": false, 518 + "identifier": "joined-aug-d", 519 + "defaultSetting": "warn" 520 + }, 521 + { 522 + "blurs": "none", 523 + "locales": [ 524 + { 525 + "lang": "en", 526 + "name": "Joined Sep ’25", 527 + "description": "This profile was created in September of 2025." 528 + } 529 + ], 530 + "severity": "inform", 531 + "adultOnly": false, 532 + "identifier": "joined-sep-d", 533 + "defaultSetting": "warn" 534 + }, 535 + { 536 + "blurs": "none", 537 + "locales": [ 538 + { 539 + "lang": "en", 540 + "name": "Joined Oct ’25", 541 + "description": "This profile was created in October of 2025." 542 + } 543 + ], 544 + "severity": "inform", 545 + "adultOnly": false, 546 + "identifier": "joined-oct-d", 547 + "defaultSetting": "warn" 548 + }, 549 + { 550 + "blurs": "none", 551 + "locales": [ 552 + { 553 + "lang": "en", 554 + "name": "Joined Nov ’25", 555 + "description": "This profile was created in November of 2025." 556 + } 557 + ], 558 + "severity": "inform", 559 + "adultOnly": false, 560 + "identifier": "joined-nov-d", 561 + "defaultSetting": "warn" 562 + }, 563 + { 564 + "blurs": "none", 565 + "locales": [ 566 + { 567 + "lang": "en", 568 + "name": "Joined Dec ’25", 569 + "description": "This profile was created in December of 2025." 570 + } 571 + ], 572 + "severity": "inform", 573 + "adultOnly": false, 574 + "identifier": "joined-dec-d", 575 + "defaultSetting": "warn" 576 + } 577 + ] 578 + }, 579 + "createdAt": "2025-02-10T06:08:05.000Z" 580 + }
+14
src/bin/database.rs
··· 1 + //! Main entrypoint. 2 + 3 + #[tokio::main(flavor = "current_thread")] 4 + async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 5 + let subscriber = tracing_subscriber::fmt() 6 + .compact() // Use a more compact, abbreviated log format 7 + .with_file(true) // Display source code file paths 8 + .with_line_number(true) // Display source code line numbers 9 + .with_thread_ids(true) // Display the thread ID an event was recorded on 10 + .with_target(false) // Don't display the event's target (module path) 11 + .finish(); // Build the subscriber 12 + tracing::subscriber::set_global_default(subscriber)?; 13 + atproto_teq::database::main_database().await 14 + }
+23
src/bin/jetstream.rs
··· 1 + //! Main entrypoint. 2 + 3 + #[tokio::main(flavor = "current_thread")] 4 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 5 + let subscriber = tracing_subscriber::fmt() 6 + .compact() // Use a more compact, abbreviated log format 7 + .with_file(true) // Display source code file paths 8 + .with_line_number(true) // Display source code line numbers 9 + .with_thread_ids(true) // Display the thread ID an event was recorded on 10 + .with_target(false) // Don't display the event's target (module path) 11 + .finish(); // Build the subscriber 12 + tracing::subscriber::set_global_default(subscriber)?; 13 + loop { 14 + if let Err(e) = atproto_teq::jetstream::main_jetstream().await { 15 + tracing::error!("Error in main_jetstream: {:?}", e); 16 + break; 17 + } else { 18 + tracing::info!("Restarting main_jetstream"); 19 + tokio::time::sleep(tokio::time::Duration::from_secs(15)).await; 20 + } 21 + } 22 + Ok(()) 23 + }
+24
src/bin/negation.rs
··· 1 + //! Main entrypoint. 2 + 3 + use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool}; 4 + use std::str::FromStr; 5 + 6 + #[tokio::main(flavor = "current_thread")] 7 + async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 8 + let subscriber = tracing_subscriber::fmt() 9 + .compact() // Use a more compact, abbreviated log format 10 + .with_file(true) // Display source code file paths 11 + .with_line_number(true) // Display source code line numbers 12 + .with_thread_ids(true) // Display the thread ID an event was recorded on 13 + .with_target(false) // Don't display the event's target (module path) 14 + .finish(); // Build the subscriber 15 + tracing::subscriber::set_global_default(subscriber)?; 16 + 17 + const NEGATION_ID: &str = "did:plc:"; // TODO: Argumentize this. 18 + let mut agent = atproto_teq::webrequest::Agent::default(); 19 + let pool_opts = SqliteConnectOptions::from_str("sqlite://prod.db").expect("Expected to be able to configure the database, but failed.") 20 + .journal_mode(SqliteJournalMode::Wal); 21 + let pool = SqlitePool::connect_with(pool_opts).await.expect("Expected to be able to connect to the database at sqlite://prod.db but failed."); 22 + atproto_teq::database::negation(NEGATION_ID, &mut agent, &pool).await?; 23 + Ok(()) 24 + }
+16
src/bin/webserve.rs
··· 1 + //! Main entrypoint. 2 + #![expect(let_underscore_drop)] 3 + 4 + #[tokio::main(flavor = "current_thread")] 5 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 6 + let subscriber = tracing_subscriber::fmt() 7 + .compact() // Use a more compact, abbreviated log format 8 + .with_file(true) // Display source code file paths 9 + .with_line_number(true) // Display source code line numbers 10 + .with_thread_ids(true) // Display the thread ID an event was recorded on 11 + .with_target(false) // Don't display the event's target (module path) 12 + .finish(); // Build the subscriber 13 + tracing::subscriber::set_global_default(subscriber)?; 14 + let _ = tokio::spawn(atproto_teq::webserve::main_webserve()).await; 15 + Ok(()) 16 + }
+142
src/crypto.rs
··· 1 + //! # Crypto 2 + //! Sign messages using the secp256k1 elliptic curve algorithm. 3 + 4 + use secp256k1::{Secp256k1, Message, SecretKey, PublicKey}; 5 + use secp256k1::hashes::{sha256, Hash}; 6 + use std::str::FromStr; 7 + 8 + use crate::types::{AssignedLabelResponse, RetrievedLabelResponse, SignatureBytes, SignatureEnum}; 9 + 10 + 11 + /// Cryptographic signing and verification. 12 + pub struct Crypto { 13 + private_key: SecretKey, 14 + _public_key: PublicKey, 15 + } 16 + impl Default for Crypto { 17 + fn default() -> Self { 18 + Self::new() 19 + } 20 + } 21 + impl Crypto { 22 + /// Create a new `Crypto` instance. 23 + pub fn new() -> Self { 24 + let secp = Secp256k1::new(); 25 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 26 + let private_key_hex = std::env::var("PRIVATE_KEY_HEX").expect("Expected to be able to get a private key from the environment, but failed"); 27 + let private_key_vec = hex::decode(private_key_hex).expect("Expected to be able to decode a hex string, but failed"); 28 + let private_key_array: [u8; 32] = private_key_vec.as_slice().try_into().expect("Expected 32 bytes, within curve order, but failed"); 29 + let private_key = SecretKey::from_slice(&private_key_array).expect("Expected 32 bytes, within curve order, but failed"); 30 + let public_key = PublicKey::from_secret_key(&secp, &private_key); 31 + Self { 32 + private_key, 33 + _public_key: public_key, 34 + } 35 + } 36 + /// Create a new `Crypto` instance from a slice. 37 + pub fn from_slice(slice: &[u8]) -> Self { 38 + let secp = Secp256k1::new(); 39 + let private_key = SecretKey::from_slice(slice).expect("Expected 32 bytes, within curve order, but failed"); 40 + let public_key = PublicKey::from_secret_key(&secp, &private_key); 41 + Self { 42 + private_key, 43 + _public_key: public_key, 44 + } 45 + } 46 + /// Sign a message. 47 + #[expect(clippy::cognitive_complexity)] 48 + pub fn sign(&self, label: &mut AssignedLabelResponse) { 49 + let secp = Secp256k1::new(); 50 + let label_for_serialization = RetrievedLabelResponse { 51 + cts: label.cts.clone(), 52 + neg: label.neg, 53 + src: label.src.clone(), 54 + uri: label.uri.clone(), 55 + val: label.val.clone(), 56 + ver: label.ver, 57 + }; 58 + tracing::debug!("Label for serialization: {:?}", label_for_serialization); 59 + label.sig = None; 60 + // let label_json = serde_json::to_string(&label_for_serialization).unwrap(); 61 + // let digest = sha256::Hash::hash(msg.as_bytes()); 62 + // let message = Message::from_digest(digest.to_byte_array()); 63 + let label_cbor = serde_cbor::to_vec(&label_for_serialization).expect("Expected to be able to serialize a label, but failed"); 64 + tracing::debug!("Label CBOR: {:?}", label_cbor); 65 + // decode the cbor we just made 66 + let label_decoded: RetrievedLabelResponse = serde_cbor::from_slice(&label_cbor).expect("Expected to be able to deserialize a label, but failed"); 67 + tracing::debug!("Label decoded: {:?}", label_decoded); 68 + let digest = sha256::Hash::hash(&label_cbor); 69 + let message = Message::from_digest(digest.to_byte_array()); 70 + let sig = secp.sign_ecdsa(&message, &self.private_key); 71 + // verify the sig we just made: 72 + let verified = secp.verify_ecdsa(&message, &sig, &self._public_key); 73 + assert!(verified.is_ok()); 74 + tracing::debug!("Verified: {:?}", verified); 75 + tracing::debug!("Message: {:?}", message); 76 + tracing::debug!("Signature: {:?}", sig); 77 + tracing::debug!("Public key: {:?}", self._public_key); 78 + // let serialized_sig = sig.serialize_der(); 79 + // return raw 64 byte sig not DER-encoded 80 + let serialized_sig: [u8; 64] = sig.serialize_compact(); 81 + // serialized_sig.to_vec() 82 + label.sig = Some(SignatureEnum::Bytes(SignatureBytes::from_bytes(serialized_sig))); 83 + } 84 + /// Consume a label response and validate the signature. 85 + #[expect(clippy::cognitive_complexity)] 86 + pub fn validate(&self, 87 + label: RetrievedLabelResponse, 88 + sig: &str, 89 + public_key_string: &str, // multibase-encoded string 90 + ) -> bool { 91 + tracing::debug!("Retrieved label: {:?}", label); 92 + // let public_key_vec = hex::decode(public_key_string).unwrap(); 93 + // When encoding public keys as strings, the preferred representation uses multibase (with base58btc specifically) and a multicode prefix to indicate the specific key type. By embedding metadata about the type of key in the encoding itself, they can be parsed unambiguously. 94 + // The process for encoding a public key in this format is: 95 + // Encode the public key curve "point" as bytes. Be sure to use the smaller "compact" or "compressed" representation. 96 + // Prepend the appropriate curve multicodec value, as varint-encoded bytes, in front of the key bytes 97 + // p256 (compressed, 33 byte key length): p256-pub, code 0x1200, varint-encoded bytes: [0x80, 0x24] 98 + // k256 (compressed, 33 byte key length): secp256k1-pub, code 0xE7, varint bytes: [0xE7, 0x01] 99 + // Encode the combined bytes with with base58btc, and prefix with a z character, yielding a multibase-encoded string 100 + // The decoding process is the same in reverse, using the identified curve type as context. 101 + let public_key_string = public_key_string.strip_prefix("z").expect("Expected to be able to strip a prefix, but failed"); 102 + let public_key_vec = bs58::decode(public_key_string).into_vec().expect("Expected to be able to decode a base58 string, but failed"); 103 + // // Remove the multicodec prefix 104 + // let public_key_vec = public_key_vec[2..].to_vec(); 105 + // Determine which curve the key is for 106 + match public_key_vec[0] { 107 + 0x80 => { 108 + tracing::debug!("p256"); 109 + // p256 110 + }, 111 + 0xE7 => { 112 + tracing::debug!("k256"); 113 + // k256 114 + }, 115 + _ => { 116 + panic!("Unknown curve"); 117 + }, 118 + }; 119 + let public_key_vec = public_key_vec[2..].to_vec(); 120 + 121 + let public_key_array: [u8; 33] = public_key_vec.as_slice().try_into().expect("Expected 33 bytes, within curve order, but failed"); 122 + let public_key = PublicKey::from_slice(&public_key_array).expect("Expected 33 bytes, within curve order, but failed"); 123 + // use of the "low-S" signature variant is required 124 + // let secp = Secp256k1::new(); 125 + let secp = Secp256k1::verification_only(); 126 + // let label_json = serde_json::to_string(&label).unwrap(); 127 + // tracing::debug!("Label JSON: {:?}", label_json); 128 + let label_cbor = serde_cbor::to_vec(&label).expect("Expected to be able to serialize a label, but failed"); 129 + let digest = sha256::Hash::hash(&label_cbor); 130 + tracing::debug!("Digest: {:?}", digest); 131 + let message = Message::from_digest(digest.to_byte_array()); 132 + tracing::debug!("Signature: {:?}", sig); 133 + let sig = SignatureBytes::from_str(sig).expect("Expected to be able to parse a signature from a string, but failed"); 134 + tracing::debug!("Signature bytes: {:?}", sig); 135 + let signature = secp256k1::ecdsa::Signature::from_compact(&sig.as_vec()).expect("Expected to be able to parse a signature from a byte array, but failed"); 136 + tracing::debug!("Message: {:?}", message); 137 + tracing::debug!("Signature: {:?}", signature); 138 + tracing::debug!("Public key: {:?}", public_key); 139 + secp.verify_ecdsa(&message, &signature, &public_key).is_ok() 140 + } 141 + 142 + }
+262
src/database.rs
··· 1 + //! This module is responsible for handling the database operations. 2 + 3 + use sqlx::{sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool}, Executor}; 4 + use std::str::FromStr; 5 + 6 + use crate::{types, webrequest::Agent}; 7 + 8 + /// The main function for the database module. 9 + #[tracing::instrument] 10 + pub async fn main_database() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 11 + const STATE: u8 = 0; 12 + let pool_opts = SqliteConnectOptions::from_str("sqlite://prod.db").expect("Expected to be able to configure the database, but failed.") 13 + .journal_mode(SqliteJournalMode::Wal); 14 + let pool = SqlitePool::connect_with(pool_opts).await.expect("Expected to be able to connect to the database at sqlite://prod.db but failed."); 15 + match STATE { 16 + 0 => { 17 + if initialize_database(&pool).await.is_err() { 18 + tracing::debug!("Database already initialized"); 19 + } 20 + }, 21 + 1 => { 22 + validate_labels(&mut Agent::default(), &pool).await? 23 + }, 24 + _ => (), 25 + } 26 + Ok(()) 27 + } 28 + async fn initialize_database(pool: &SqlitePool) -> Result<(), sqlx::Error> { 29 + tracing::debug!("Initializing database"); 30 + let mut connection = pool.acquire().await?; 31 + _ = connection.execute("PRAGMA foreign_keys=on").await?; 32 + _ = connection 33 + .execute("CREATE TABLE profile (did STRING PRIMARY KEY)") 34 + .await?; 35 + _ = connection 36 + .execute( 37 + "CREATE TABLE profile_stats ( 38 + did STRING PRIMARY KEY, 39 + created_at DATETIME NOT NULL, 40 + follower_count INTEGER NOT NULL, 41 + post_count INTEGER NOT NULL, 42 + checked_at DATETIME NOT NULL, 43 + FOREIGN KEY(did) REFERENCES profile (did) 44 + )", 45 + ) 46 + .await?; 47 + _ = connection 48 + .execute( 49 + "CREATE TABLE profile_labels ( 50 + seq INTEGER PRIMARY KEY AUTOINCREMENT, 51 + uri STRING NOT NULL, 52 + cid STRING, 53 + val STRING NOT NULL, 54 + neg BOOLEAN, 55 + cts DATETIME NOT NULL, 56 + exp DATETIME, 57 + sig BLOB NOT NULL, 58 + FOREIGN KEY(uri) REFERENCES profile (did) 59 + )", 60 + ) 61 + .await?; 62 + tracing::info!("Database initialized"); 63 + Ok(()) 64 + } 65 + /// Negate a label by its DID. 66 + pub async fn negation(negation_id: &str, agent: &mut Agent, pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 67 + let existing_label = sqlx::query!( 68 + r#"SELECT seq FROM profile_labels 69 + WHERE uri = ? AND neg is not true 70 + "#, 71 + negation_id, 72 + ).fetch_optional(pool) 73 + .await.expect("Expected to be able to fetch the label, but failed."); 74 + tracing::info!("Existing label: {:?}", existing_label); 75 + if let Some(label) = existing_label { 76 + tracing::info!("Removing label for {}", negation_id); 77 + drop(types::Profile::remove_label(pool, label.seq).await); 78 + } 79 + tracing::info!("Inserting negate for {}", negation_id); 80 + drop(types::Profile::new(negation_id).determine_stats(agent, pool).await.negate_label(pool).await); 81 + Ok(()) 82 + } 83 + /// Iterate over all rows of profile_labels and validate the labels. 84 + async fn validate_labels(agent: &mut Agent, pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 85 + // // cleanup duplicates 86 + // let uris_with_duplicates = sqlx::query!( 87 + // r#"SELECT uri FROM profile_labels 88 + // GROUP BY uri 89 + // HAVING COUNT(*) > 1 90 + // "#, 91 + // ).fetch_all(pool) 92 + // .await.expect("Expected to be able to fetch all labels, but failed."); 93 + // for uri in uris_with_duplicates { 94 + // let seqs = sqlx::query!( 95 + // r#"SELECT seq FROM profile_labels 96 + // WHERE uri = ? 97 + // ORDER BY seq ASC 98 + // "#, 99 + // uri.uri, 100 + // ).fetch_all(pool) 101 + // .await.expect("Expected to be able to fetch all labels, but failed."); 102 + // let mut first = true; 103 + // for seq in seqs { 104 + // if first { 105 + // first = false; 106 + // continue; 107 + // } 108 + // let seq = seq.seq; 109 + // let _ = sqlx::query!( 110 + // r#"DELETE FROM profile_labels 111 + // WHERE seq = ? 112 + // "#, 113 + // seq, 114 + // ).execute(pool).await.expect("Expected to be able to delete the label, but failed."); 115 + // } 116 + // } 117 + let mut valid = 0; 118 + let mut invalid = 0; 119 + let mut invalid_list = Vec::new(); 120 + let mut unreachable = 0; 121 + let start_from_seq = 222_081; // TODO: argumentize this 122 + let go_to_seq = 222_144; // TODO: argumentize this 123 + let labels = sqlx::query!( 124 + r#"SELECT seq, uri "uri: String", neg FROM profile_labels 125 + WHERE seq > ? AND seq < ? 126 + "#, 127 + start_from_seq, 128 + go_to_seq, 129 + ) 130 + .fetch_all(pool) 131 + .await.expect("Expected to be able to fetch all labels, but failed."); 132 + let num_of_labels = labels.len(); 133 + tracing::info!("Validating {} labels", num_of_labels); 134 + let mut count: u32 = 0; 135 + // We'll be collecting 25 labels at a time, and then checking them with get_profiles, to avoid rate limiting. 136 + let mut label_list = Vec::new(); 137 + const FETCH_AMOUNT: usize = 25; 138 + for label in labels { 139 + if label.neg == Some(true) { 140 + tracing::info!("Skipping negative label seq {} for https://bsky.app/profile/{}", label.seq, label.uri); 141 + continue; 142 + } 143 + label_list.push((label.uri, label.seq)); 144 + if label_list.len() == FETCH_AMOUNT { 145 + let fetched_labels = agent.check_profiles(label_list.as_slice()).await.expect("Expected to be able to check the profiles, but failed."); 146 + if fetched_labels.len() != FETCH_AMOUNT { 147 + tracing::warn!("Expected to get {} labels, but got {}", FETCH_AMOUNT, fetched_labels.len()); 148 + unreachable += FETCH_AMOUNT - fetched_labels.len(); 149 + // TODO: Figure out which ones are missing 150 + } 151 + for fetched_label in fetched_labels.iter() { 152 + let uri = fetched_label.1.0.as_str(); 153 + let seq = fetched_label.1.1; 154 + count += 1; 155 + if count % 100 == 0 { 156 + tracing::info!("Validating label count {}, seq {}", count, seq); 157 + } 158 + if fetched_label.0 { 159 + tracing::debug!("Valid label seq {} for https://bsky.app/profile/{}", seq, uri); 160 + valid += 1; 161 + } else { 162 + tracing::warn!("Invalid label seq {} for https://bsky.app/profile/{}", seq, uri); 163 + invalid += 1; 164 + invalid_list.push(format!("https://bsky.app/profile/{}", uri)); 165 + // let mut profile = types::Profile::new(uri); 166 + // let spawned_pool = pool.clone(); 167 + // // drop(tokio::spawn(async move { 168 + // if types::Profile::remove_label(&spawned_pool, seq).await.is_ok() 169 + // && profile.determine_stats_exist(&spawned_pool.clone()).await.expect( 170 + // "Expected to be able to determine if stats exist, but failed.").is_some() { 171 + // _ = profile.determine_label(&spawned_pool).await; 172 + // tracing::debug!("Label removed and profile revalidated for https://bsky.app/profile/{}", uri); 173 + // } else { 174 + // tracing::warn!("Failed to remove label {} for https://bsky.app/profile/{}", seq, uri); 175 + // } 176 + // })); 177 + } 178 + // { 179 + // tracing::warn!("Failed to get profile https://bsky.app/profile/{}", uri); 180 + // unreachable += 1; 181 + // let spawned_pool = pool.clone(); 182 + // // drop(tokio::spawn(async move { 183 + // if types::Profile::remove_label(&spawned_pool, seq).await.is_err() { 184 + // tracing::warn!("Failed to remove label {} for https://bsky.app/profile/{}", seq, uri); 185 + // } 186 + // // })); 187 + // } 188 + } 189 + label_list.clear(); 190 + // tokio::time::sleep(tokio::time::Duration::from_millis(5)).await; 191 + } 192 + } 193 + 194 + // for label in labels { 195 + // tokio::time::sleep(tokio::time::Duration::from_millis(25)).await; // Sleep for 25ms to throttle rate limiting 196 + // count += 1; 197 + // if count % 100 == 0 { 198 + // tracing::info!("Validating label count {}, seq {}", count, label.seq); 199 + // } 200 + // tracing::debug!("Validating label seq {} for {}", label.seq, label.uri); 201 + // let valid_label = agent.check_profile(&label.uri).await; 202 + // if valid_label.is_ok() { 203 + // if valid_label.expect("Expected to be able to check the profile, but failed.") { 204 + // valid += 1; 205 + // } else { 206 + // tracing::warn!("Invalid label seq {} for https://bsky.app/profile/{}", label.seq, label.uri); 207 + // invalid += 1; 208 + // invalid_list.push(format!("https://bsky.app/profile/{}", label.uri)); 209 + // let mut profile = crate::types::Profile::new(&label.uri); 210 + // let spawned_pool = pool.clone(); 211 + // drop(tokio::spawn(async move { 212 + // if crate::types::Profile::remove_label(&spawned_pool, label.seq).await.is_ok() 213 + // && profile.determine_stats_exist(&spawned_pool.clone()).await.expect( 214 + // "Expected to be able to determine if stats exist, but failed.").is_some() { 215 + // _ = profile.determine_label(&spawned_pool).await; 216 + // tracing::debug!("Label removed and profile revalidated for https://bsky.app/profile/{}", label.uri); 217 + // } else { 218 + // tracing::warn!("Failed to remove label {} for https://bsky.app/profile/{}", label.seq, label.uri); 219 + // } 220 + // })); 221 + // } 222 + // } else { 223 + // let valid_label2 = agent.check_profile(&label.uri).await; 224 + // if valid_label2.is_ok() { 225 + // if valid_label2.expect("Expected to be able to check the profile, but failed.") { 226 + // tracing::warn!("Had to retry for profile https://bsky.app/profile/{}", label.uri); 227 + // valid += 1; 228 + // } else { 229 + // tracing::warn!("Invalid label {} for https://bsky.app/profile/{}", label.seq, label.uri); 230 + // invalid += 1; 231 + // invalid_list.push(format!("https://bsky.app/profile/{}", label.uri)); 232 + // let mut profile = crate::types::Profile::new(&label.uri); 233 + // let spawned_pool = pool.clone(); 234 + // drop(tokio::spawn(async move { 235 + // if crate::types::Profile::remove_label(&spawned_pool, label.seq).await.is_ok() 236 + // && profile.determine_stats_exist(&spawned_pool.clone()).await.expect( 237 + // "Expected to be able to determine if stats exist, but failed.").is_none() { 238 + // _ = profile.determine_label(&spawned_pool).await; 239 + // tracing::info!("Label removed and profile revalidated for https://bsky.app/profile/{}", label.uri); 240 + // } else { 241 + // tracing::warn!("Failed to remove label {} for https://bsky.app/profile/{}", label.seq, label.uri); 242 + // } 243 + // })); 244 + // } 245 + // } else { 246 + // tracing::warn!("Failed to get profile https://bsky.app/profile/{}", label.uri); 247 + // unreachable += 1; 248 + // let spawned_pool = pool.clone(); 249 + // drop(tokio::spawn(async move { 250 + // if crate::types::Profile::remove_label(&spawned_pool, label.seq).await.is_err() { 251 + // tracing::warn!("Failed to remove label {} for https://bsky.app/profile/{}", label.seq, label.uri); 252 + // } 253 + // })); 254 + // } 255 + // } 256 + // } 257 + tracing::info!("Valid labels: {}", valid); 258 + tracing::info!("Invalid labels: {}", invalid); 259 + tracing::info!("List of invalid labels: {:?}", invalid_list); 260 + tracing::info!("Unreachable labels: {}", unreachable); 261 + Ok(()) 262 + }
+197
src/jetstream.rs
··· 1 + //! Consume Jetstream events. 2 + use std::{str::FromStr, sync::Arc}; 3 + 4 + use atrium_api::{record::KnownRecord::AppBskyFeedPost, types::string}; 5 + use jetstream_oxide::{ 6 + DefaultJetstreamEndpoints, JetstreamCompression, JetstreamConfig, JetstreamConnector, 7 + events::{JetstreamEvent::Commit, commit::CommitEvent}, 8 + }; 9 + use sqlx::{sqlite::{SqliteConnectOptions, SqliteJournalMode}, SqlitePool}; 10 + use tokio::runtime::Handle; 11 + 12 + use crate::{database::negation, types::{Profile, ProfileStats}, webrequest::Agent}; 13 + /// Consume Jetstream events. 14 + #[tracing::instrument] 15 + pub async fn main_jetstream() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 16 + let metrics = Handle::current().metrics(); 17 + let pool_opts = SqliteConnectOptions::from_str("sqlite://prod.db").expect("Expected to be able to configure the database, but failed.") 18 + .journal_mode(SqliteJournalMode::Wal); 19 + let pool = SqlitePool::connect_with(pool_opts).await.expect("Expected to be able to connect to the database at sqlite://prod.db but failed."); 20 + let mut agent: Agent = Agent::default(); 21 + let app_bsky_feed_post: string::Nsid = "app.bsky.feed.post".parse().expect("Expected to be able to parse a string, but failed"); 22 + let config = JetstreamConfig { 23 + endpoint: DefaultJetstreamEndpoints::USEastOne.into(), 24 + wanted_collections: vec![app_bsky_feed_post], 25 + wanted_dids: vec![], 26 + compression: JetstreamCompression::Zstd, 27 + cursor: None, 28 + }; 29 + let jetstream = JetstreamConnector::new(config).expect("Failed to connect to Jetstream"); 30 + let receiver = jetstream 31 + .connect() 32 + .await 33 + .expect("Failed to connect to Jetstream"); 34 + let language = string::Language::from_str("en").expect("Expected to be able to parse a string, but failed"); 35 + 36 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 37 + let negation_post_uri = dotenvy::var("NEGATION_POST_URI").expect("Expected to be able to read a variable, but failed."); 38 + automated_removal(&pool, &mut agent, negation_post_uri.as_str()).await?; 39 + let addition_post_uri = dotenvy::var("ADDITION_POST_URI").expect("Expected to be able to read a variable, but failed."); 40 + manual_addition(&pool, &mut agent, addition_post_uri.as_str()).await?; 41 + 42 + const CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(20); 43 + let mut last_check = tokio::time::Instant::now(); 44 + 45 + let mut vec_of_profiles_to_check: Vec<String> = vec![]; 46 + 47 + while let Ok(event) = receiver.recv_async().await { 48 + if let Commit(CommitEvent::Create { info, commit }) = event { 49 + match commit.record { 50 + AppBskyFeedPost(record) => { 51 + if record.langs == Some(vec![(language).clone()]) { 52 + let profile = Profile::new(info.did.clone().as_str()).insert_profile(&pool.clone()).await; 53 + match profile 54 + { 55 + Ok(mut profile) => { 56 + if profile.determine_stats_exist(&pool.clone()).await?.is_none() { 57 + vec_of_profiles_to_check.push(info.did.clone().to_string()); 58 + } else { 59 + tracing::debug!("Stats already exist: {:?}", info.did.as_str()); 60 + } 61 + } 62 + Err(_e) => { 63 + // tracing::debug!("Duplicate profile: {:?}", info.did.as_str()); 64 + } 65 + } 66 + } 67 + } 68 + // atrium_api::record::KnownRecord::AppBskyFeedLike(like) => { 69 + // match like.subject.uri.as_str() { 70 + // NEGATION_POST_URI => { 71 + // negation(info.did.as_str(), &mut agent, &pool).await?; 72 + // }, 73 + // ADDITION_POST_URI => { 74 + // let mut profile = Profile::new(info.did.as_str()).insert_profile(&pool).await?; 75 + // let mut profile_with_stats = profile.determine_stats_exist(&pool).await?; 76 + // if profile_with_stats.is_none() { 77 + // profile_with_stats = Some(profile.determine_stats(&mut agent, &pool).await); 78 + // } 79 + // if let Some(profile_with_stats) = profile_with_stats { 80 + // _ = profile_with_stats.determine_label_agnostic(&pool).await; 81 + // } 82 + // } 83 + // _ => {} 84 + // } 85 + // } 86 + _ => {} 87 + } 88 + } 89 + if vec_of_profiles_to_check.len() >= 25 { 90 + match_profiles(&agent, &pool, &metrics, &vec_of_profiles_to_check).await?; 91 + vec_of_profiles_to_check.clear(); 92 + } 93 + if last_check.elapsed() > CHECK_INTERVAL { 94 + automated_removal(&pool, &mut agent, negation_post_uri.as_str()).await?; 95 + manual_addition(&pool, &mut agent, addition_post_uri.as_str()).await?; 96 + last_check = tokio::time::Instant::now(); 97 + } 98 + } 99 + tracing::info!("Jetstream event stream ended unexpectedly."); 100 + Ok(()) 101 + } 102 + 103 + /// Every so often, we'll check the `app.bsky.feed.getLikes` endpoint for new likes on our provided `uri` for requested negation labels. 104 + async fn automated_removal( 105 + pool: &SqlitePool, 106 + agent: &mut Agent, 107 + negation_post_uri: &str, 108 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 109 + let likes = agent.get_likes(negation_post_uri).await?; 110 + for like in likes { 111 + let did = like["actor"]["did"].as_str().expect("Expected to be able to parse a string, but failed"); 112 + negation(did, agent, pool).await?; 113 + } 114 + Ok(()) 115 + } 116 + async fn manual_addition( 117 + pool: &SqlitePool, 118 + agent: &mut Agent, 119 + addition_post_uri: &str, 120 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 121 + let likes = agent.get_likes(addition_post_uri).await?; 122 + for like in likes { 123 + let did = like["actor"]["did"].as_str().expect("Expected to be able to parse a string, but failed"); 124 + let mut profile = Profile::new(did).insert_profile(pool).await?; 125 + let mut profile_with_stats = profile.determine_stats_exist(pool).await?; 126 + if profile_with_stats.is_none() { 127 + profile_with_stats = Some(profile.determine_stats(agent, pool).await); 128 + } 129 + if let Some(profile_with_stats) = profile_with_stats { 130 + _ = profile_with_stats.determine_label_agnostic(pool).await; 131 + } 132 + } 133 + Ok(()) 134 + } 135 + // async fn match_profile( 136 + // agent: &Agent, 137 + // pool: &SqlitePool, 138 + // metrics: &tokio::runtime::RuntimeMetrics, 139 + // did_str: &str, 140 + // ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 141 + // let mut profile = Profile::new(did_str); 142 + // let num_alive_tasks = metrics.num_alive_tasks(); 143 + // if num_alive_tasks < 50 { 144 + // let spawned_pool = pool.clone(); 145 + // let mut spawned_agent = agent.clone(); 146 + // drop(tokio::spawn(async move { 147 + // _ = profile.determine_stats(&mut spawned_agent, &spawned_pool).await.determine_label(&spawned_pool).await; 148 + // })); 149 + // } else { 150 + // tracing::warn!("Too many tasks alive: {:?}", num_alive_tasks); 151 + // } 152 + // Ok(()) 153 + // } 154 + /// Match a vec of profiles 155 + async fn match_profiles( 156 + agent: &Agent, 157 + pool: &SqlitePool, 158 + metrics: &tokio::runtime::RuntimeMetrics, 159 + did_strs: &[String], 160 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 161 + let num_alive_tasks = metrics.num_alive_tasks(); 162 + if num_alive_tasks < 14 { 163 + let spawned_pool = pool.clone(); 164 + let mut spawned_agent = Agent { 165 + access_jwt: Arc::clone(&agent.access_jwt), 166 + refresh_jwt: Arc::clone(&agent.refresh_jwt), 167 + client: agent.client.clone(), 168 + self_did: agent.self_did.clone(), 169 + }; 170 + let did_strs = did_strs.to_owned(); 171 + drop(tokio::spawn(async move { 172 + tracing::info!("Checking profiles: {:?}", did_strs); 173 + let profiles_vec = spawned_agent.get_profiles(&did_strs).await.expect("Expected to be able to get profiles, but failed."); 174 + let profiles_array = profiles_vec["profiles"].as_array(); 175 + if profiles_array.is_none() { 176 + tracing::warn!("No profiles json found for profiles: {:?}", profiles_vec); 177 + return; 178 + } 179 + for profile_stats in profiles_array.unwrap_or_else(|| panic!("Expected to be able to read profiles as array, but failed. Profiles: {:?}", profiles_vec)) { 180 + let did = profile_stats["did"].as_str().expect("Expected to be able to parse a string, but failed"); 181 + let mut profile = Profile::new(did).insert_profile(&spawned_pool).await.expect("Expected to be able to insert a profile, but failed."); 182 + let checked_at = chrono::Utc::now(); 183 + profile.stats = Some(ProfileStats { 184 + follower_count: profile_stats["followersCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32, 185 + post_count: profile_stats["postsCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32, 186 + created_at: chrono::DateTime::parse_from_rfc3339(profile_stats["createdAt"].as_str().expect("Expected to be able to parse a string, but failed")).expect("Expected to be able to parse a string, but failed").into(), 187 + checked_at, 188 + }); 189 + profile.insert_profile_stats(&spawned_pool).await.expect("Expected to be able to insert profile stats, but failed."); 190 + _ = profile.determine_label(&spawned_pool).await; 191 + } 192 + })); 193 + } else { 194 + tracing::warn!("Too many tasks alive: {:?}", num_alive_tasks); 195 + } 196 + Ok(()) 197 + }
+7
src/lib.rs
··· 1 + //! Library reexports. 2 + pub mod database; 3 + pub mod jetstream; 4 + pub mod types; 5 + pub mod webrequest; 6 + pub mod webserve; 7 + pub mod crypto;
+806
src/types.rs
··· 1 + //! Structs, enums, and impls. 2 + use std::str::FromStr; 3 + use base64::Engine; 4 + use chrono::{DateTime as Datetime, Datelike}; 5 + use jetstream_oxide::exports::Did; 6 + use serde::{ser::{Serialize, Serializer}, Deserialize}; 7 + 8 + // /// How should a client visually convey this label? 9 + // enum LabelDefinitionSeverity { 10 + // /// 'inform' means neutral and informational 11 + // Inform, 12 + // /// 'alert' means negative and warning 13 + // Alert, 14 + // /// 'none' means show nothing. 15 + // None, 16 + // } 17 + // impl LabelDefinitionSeverity { 18 + // fn to_string(&self) -> String { 19 + // match self { 20 + // Self::Inform => "inform".to_owned(), 21 + // Self::Alert => "alert".to_owned(), 22 + // Self::None => "none".to_owned(), 23 + // } 24 + // } 25 + // } 26 + // /// What should this label hide in the UI, if applied? 27 + // enum LabelDefinitionBlurs { 28 + // /// 'content' hides all of the target 29 + // Content, 30 + // /// 'media' hides the images/video/audio 31 + // Media, 32 + // /// 'none' hides nothing. 33 + // None, 34 + // } 35 + // impl LabelDefinitionBlurs { 36 + // fn to_string(&self) -> String { 37 + // match self { 38 + // Self::Content => "content".to_owned(), 39 + // Self::Media => "media".to_owned(), 40 + // Self::None => "none".to_owned(), 41 + // } 42 + // } 43 + // } 44 + // /// The default setting for this label. 45 + // enum LabelDefinitionDefaultSetting { 46 + // Hide, 47 + // Warn, 48 + // Ignore, 49 + // } 50 + // impl LabelDefinitionDefaultSetting { 51 + // fn to_string(&self) -> String { 52 + // match self { 53 + // Self::Hide => "hide".to_owned(), 54 + // Self::Warn => "warn".to_owned(), 55 + // Self::Ignore => "ignore".to_owned(), 56 + // } 57 + // } 58 + // } 59 + // /// Strings which describe the label in the UI, localized into a specific language. 60 + // struct LabelValueDefinitionStrings { 61 + // /// The code of the language these strings are written in. 62 + // lang: String, 63 + // /// A short human-readable name for the label. 64 + // name: String, 65 + // /// A longer description of what the label means and why it might be applied. 66 + // description: String, 67 + // } 68 + // /// Labels. 69 + // struct LabelDefinition { 70 + // /// The value of the label being defined. Must only include lowercase ascii and the '-' character (a-z-+). 71 + // identifier: String, 72 + // /// How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. 73 + // severity: LabelDefinitionSeverity, 74 + // /// What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. 75 + // blurs: LabelDefinitionBlurs, 76 + // /// The default setting for this label. 77 + // default_setting: LabelDefinitionDefaultSetting, 78 + // /// Does the user need to have adult content enabled in order to configure this label? 79 + // adult_content: Option<bool>, 80 + // /// Strings which describe the label in the UI, localized into a specific language. 81 + // locales: Vec<LabelValueDefinitionStrings>, 82 + // } 83 + // impl LabelDefinition { 84 + // fn new(identifier: String) -> Self { 85 + // let locales = vec![LabelValueDefinitionStrings { 86 + // lang: "en".to_owned(), 87 + // name: identifier.replace("joined-", "Joined "), 88 + // description: format!("Profile created {}", identifier.replace("joined-", "").replace("-", " ")), 89 + // }]; 90 + // Self { 91 + // identifier, 92 + // severity: LabelDefinitionSeverity::Inform, 93 + // blurs: LabelDefinitionBlurs::None, 94 + // default_setting: LabelDefinitionDefaultSetting::Warn, 95 + // adult_content: Some(false), 96 + // locales, 97 + // } 98 + // } 99 + // } 100 + 101 + 102 + #[derive(Debug)] 103 + /// Signature bytes. 104 + pub struct SignatureBytes([u8; 64]); 105 + impl FromStr for SignatureBytes { 106 + type Err = std::io::Error; 107 + fn from_str(s: &str) -> Result<Self, Self::Err> { 108 + let bytes = base64::engine::GeneralPurpose::new( 109 + &base64::alphabet::STANDARD, 110 + base64::engine::general_purpose::NO_PAD).decode(s).expect("Expected to be able to decode the base64 string as bytes but failed."); 111 + let mut array = [0; 64]; 112 + array.copy_from_slice(&bytes); 113 + Ok(Self(array)) 114 + } 115 + } 116 + impl SignatureBytes { 117 + /// Create a new signature from a vector of bytes. 118 + pub fn from_vec(vec: Vec<u8>) -> Self { 119 + let mut array = [0; 64]; 120 + array.copy_from_slice(&vec); 121 + Self(array) 122 + } 123 + /// Create a new signature from a slice of bytes. 124 + pub const fn from_bytes(bytes: [u8; 64]) -> Self { 125 + Self(bytes) 126 + } 127 + /// Create a new signature from a JSON value in the format of a $bytes object. 128 + pub fn from_json(json: serde_json::Value) -> Self { 129 + let byte_string = json["$bytes"].as_str().expect("Expected to be able to get the $bytes field from the JSON object as a string but failed."); 130 + let bytes = base64::engine::GeneralPurpose::new( 131 + &base64::alphabet::STANDARD, 132 + base64::engine::general_purpose::NO_PAD).decode(byte_string).expect("Expected to be able to decode the base64 string as bytes but failed."); 133 + Self::from_vec(bytes) 134 + } 135 + /// Get the signature as a vector of bytes. 136 + pub fn as_vec(&self) -> Vec<u8> { 137 + self.0.to_vec() 138 + } 139 + /// Get the signature as a base64 string. 140 + pub fn as_base64(&self) -> String { 141 + base64::engine::GeneralPurpose::new( 142 + &base64::alphabet::STANDARD, 143 + base64::engine::general_purpose::NO_PAD).encode(self.0) 144 + } 145 + /// Get the signature as a JSON object in the format of a $bytes object. 146 + pub fn as_json_object(&self) -> serde_json::Value { 147 + serde_json::json!({ 148 + "$bytes": self.as_base64() 149 + }) 150 + } 151 + } 152 + impl Serialize for SignatureBytes { 153 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 154 + where 155 + S: Serializer, 156 + { 157 + serializer.serialize_bytes(&self.0) 158 + } 159 + } 160 + #[derive(Debug)] 161 + /// Signature bytes or JSON value. 162 + pub enum SignatureEnum { 163 + /// Signature bytes. 164 + Bytes(SignatureBytes), 165 + /// Signature JSON value. 166 + Json(serde_json::Value), 167 + } 168 + impl Serialize for SignatureEnum { 169 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 170 + where 171 + S: Serializer, 172 + { 173 + match self { 174 + Self::Bytes(bytes) => bytes.serialize(serializer), 175 + Self::Json(json) => json.serialize(serializer), 176 + } 177 + } 178 + } 179 + 180 + #[derive(serde::Serialize, Debug)] 181 + /// Label response content. 182 + pub struct AssignedLabelResponse { 183 + // /// Timestamp at which this label expires (no longer applies, is no longer valid). 184 + // exp: Option<DateTime<FixedOffset>>, 185 + // /// Optionally, CID specifying the specific version of 'uri' resource this label applies to. \ 186 + // /// If provided, the label applies to a specific version of the subject uri 187 + // cid: Option<String>, 188 + /// Timestamp when this label was created. 189 + /// Note that timestamps in a distributed system are not trustworthy or verified by default. 190 + pub cts: String, // DateTime<Utc>, 191 + /// If true, this is a negation label, indicates that this label "negates" an earlier label with the same src, uri, and val. 192 + /// If the neg field is false, best practice is to simply not include the field at all. 193 + #[serde(skip_serializing_if = "bool_is_false")] 194 + pub neg: bool, 195 + /// Signature of dag-cbor encoded label. \ 196 + /// cryptographic signature bytes. \ 197 + /// Uses the bytes type from the [Data Model](https://atproto.com/specs/data-model), which encodes in JSON as a $bytes object with base64 encoding 198 + /// When labels are being transferred as full objects between services, the ver and sig fields are required. 199 + pub sig: Option<SignatureEnum>, 200 + /// DID of the actor authority (account) which generated this label. \ 201 + pub src: Did, 202 + /// AT URI of the record, repository (account), or other resource that this label applies to. \ 203 + /// For a specific record, an `at://` URI. For an account, the `did:`. 204 + pub uri: String, 205 + /// The short (<=128 character) string name of the value or type of this label. 206 + pub val: String, 207 + /// The AT Protocol version of the label object schema version. \ 208 + /// Current version is always 1. 209 + /// When labels are being transferred as full objects between services, the ver and sig fields are required. 210 + pub ver: u64, 211 + } 212 + impl AssignedLabelResponse { 213 + /// Create a new label. 214 + pub fn generate( 215 + src: Did, 216 + uri: String, 217 + val: String, 218 + ) -> Self { 219 + let sig = SignatureEnum::Bytes(SignatureBytes([0; 64])); 220 + Self::reconstruct(src, uri, val, false, chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), sig) 221 + } 222 + /// Reconstruct a label from parts. 223 + pub const fn reconstruct( 224 + src: Did, 225 + uri: String, 226 + val: String, 227 + neg: bool, 228 + cts: String, 229 + sig: SignatureEnum, 230 + ) -> Self { 231 + Self { 232 + ver: 1, 233 + src, 234 + uri, 235 + // cid: None, 236 + val, 237 + neg, 238 + sig: Some(sig), 239 + cts, 240 + // exp: None, 241 + } 242 + } 243 + // The process to sign or verify a signature is to construct a complete version of the label, using only the specified schema fields, and not including the sig field. 244 + // This means including the ver field, but not any $type field or other un-specified fields which may have been included in a Lexicon representation of the label. This data object is then encoded in CBOR, following the deterministic IPLD/DAG-CBOR normalization rules. 245 + // The CBOR bytes are hashed with SHA-256, and then the direct hash bytes (not a hex-encoded string) are signed (or verified) using the appropriate cryptographic key. The signature bytes are stored in the sig field as bytes (see Data Model for details representing bytes). 246 + /// Generate signature. 247 + pub fn sign(mut self) -> Self { 248 + crate::crypto::Crypto::new().sign(&mut self); 249 + self 250 + } 251 + 252 + } 253 + #[derive(serde::Serialize)] 254 + /// A label response wrapper. 255 + pub struct AssignedLabelResponseWrapper { 256 + /// The cursor to find the sequence number of this label. \ 257 + /// Returned as a string. 258 + pub cursor: String, 259 + /// Vector of labels. 260 + pub labels: Vec<AssignedLabelResponse>, 261 + } 262 + #[derive(serde::Serialize, Debug)] 263 + /// A label response wrapper. 264 + pub struct SubscribeLabelsLabels { 265 + /// The sequence number of this label. \ 266 + /// The seq field is a monotonically increasing integer, starting at 1 for the first label. 267 + /// Returned as a long. 268 + pub seq: i64, 269 + /// Vector of labels. 270 + pub labels: Vec<AssignedLabelResponse>, 271 + } 272 + #[derive(Deserialize)] 273 + #[expect(non_snake_case, reason = "Name matches URI parameter literally.")] 274 + /// URI parameters. 275 + pub struct UriParams { 276 + /// URI patterns. 277 + pub uriPatterns: Option<String>, 278 + /// The DID of sources. 279 + pub sources: Option<String>, 280 + /// The limit of labels to fetch. Default is (50?). 281 + pub limit: Option<i64>, 282 + /// The cursor to use for seq. 283 + pub cursor: Option<String>, 284 + /// The actor to lookup. 285 + pub actor: Option<String>, 286 + } 287 + const fn neg_default() -> bool { 288 + false 289 + } 290 + 291 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 292 + /// A label retrieved from the atproto API. 293 + pub struct RetrievedLabelResponse { 294 + /// The creation timestamp. 295 + pub cts: String, 296 + /// Whether the label is negative. 297 + #[serde(skip_serializing_if = "bool_is_false", default = "neg_default")] 298 + pub neg: bool, 299 + /// The source DID. 300 + pub src: Did, 301 + /// The URI. 302 + pub uri: String, 303 + /// The value. 304 + pub val: String, 305 + /// The version. 306 + pub ver: u64, 307 + } 308 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 309 + /// A label retrieved from the atproto API. 310 + pub struct SignedRetrievedLabelResponse { 311 + /// The creation timestamp. 312 + pub cts: String, 313 + /// Whether the label is negative. 314 + #[serde(skip_serializing_if = "bool_is_false")] 315 + pub neg: bool, 316 + /// The source DID. 317 + pub sig: serde_json::Value, 318 + /// The source DID. 319 + pub src: Did, 320 + /// The URI. 321 + pub uri: String, 322 + /// The value. 323 + pub val: String, 324 + /// The version. 325 + pub ver: u64, 326 + } 327 + fn bool_is_false(b: &bool) -> bool { 328 + !b 329 + } 330 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 331 + /// A label retrieved from the atproto API. 332 + pub struct SignedRetrievedLabelResponseWs { 333 + /// The creation timestamp. 334 + pub cts: String, 335 + /// Whether the label is negative. 336 + #[serde(skip_serializing_if = "bool_is_false")] 337 + pub neg: bool, 338 + /// The signature. 339 + #[serde(with = "serde_bytes")] 340 + pub sig: [u8; 64], 341 + /// The source DID. 342 + pub src: Did, 343 + /// The URI. 344 + pub uri: String, 345 + /// The value. 346 + pub val: String, 347 + /// The version. 348 + pub ver: u64, 349 + } 350 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 351 + /// Labels with a sequence number. 352 + pub struct LabelsVecWithSeq { 353 + /// The sequence number. 354 + pub seq: u64, 355 + /// The labels. 356 + pub labels: Vec<SignedRetrievedLabelResponseWs>, 357 + } 358 + #[derive(Debug)] 359 + /// Profile stats. 360 + pub struct ProfileStats { 361 + /// The number of followers. 362 + pub follower_count: i32, 363 + /// The number of posts. 364 + pub post_count: i32, 365 + /// The creation timestamp, as reported by actor. 366 + pub created_at: Datetime<chrono::Utc>, 367 + /// The timestamp at which the stats were checked. 368 + pub checked_at: Datetime<chrono::Utc>, 369 + } 370 + impl ProfileStats { 371 + fn new( 372 + follower_count: i32, 373 + post_count: i32, 374 + created_at: Datetime<chrono::Utc>, 375 + ) -> Self { 376 + Self { 377 + follower_count, 378 + post_count, 379 + created_at, 380 + checked_at: chrono::Utc::now(), 381 + } 382 + } 383 + /// Given a AT uri, lookup the profile and return the stats. 384 + pub async fn from_at_url( 385 + uri: String, 386 + agent: &mut crate::webrequest::Agent, 387 + ) -> Result<Self, Box<dyn std::error::Error>> { 388 + let uri = uri.replace("at://","").replace("/app.bsky.actor.profile/self", ""); 389 + if let Ok(profile) = agent.get_profile(uri.as_str()).await { 390 + tracing::debug!("{:?}", profile); 391 + 392 + // Begin enforce reasonable limits on the number of follows. 393 + // https://jazco.dev/2025/02/19/imperfection/ 394 + let follows_count = profile["followsCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32; 395 + const MAX_FOLLOWS: i32 = 4_000; 396 + if follows_count > MAX_FOLLOWS { 397 + tracing::warn!("Profile {:?} has a suspicious number of follows: {:?}", uri, follows_count); 398 + return Err(Box::new(std::io::Error::new( 399 + std::io::ErrorKind::Other, 400 + "Profile has a suspicious number of follows", 401 + ))); 402 + } 403 + // End 404 + 405 + let followers_count = profile["followersCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32; 406 + let posts_count = profile["postsCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32; 407 + let created_at = Datetime::parse_from_rfc3339(profile["createdAt"].as_str().expect("Expected to be able to parse a string, but failed"))?; 408 + Ok(Self::new(followers_count, posts_count, created_at.into())) 409 + } else { 410 + Err(Box::new(std::io::Error::new( 411 + std::io::ErrorKind::Other, 412 + "Failed to get profile", 413 + ))) 414 + } 415 + } 416 + } 417 + #[derive(Debug, Clone, Copy, serde::Deserialize)] 418 + enum Year { 419 + _2022, 420 + _2023, 421 + _2024, 422 + _2025, 423 + } 424 + impl std::fmt::Display for Year { 425 + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 426 + write!( 427 + f, 428 + "{}", 429 + match self { 430 + Self::_2022 => "a", 431 + Self::_2023 => "b", 432 + Self::_2024 => "c", 433 + Self::_2025 => "d", 434 + } 435 + ) 436 + } 437 + } 438 + impl Serialize for Year { 439 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 440 + where 441 + S: Serializer, 442 + { 443 + let val: char = self.to_string().chars().next().expect("Expected to be able to get the first character, but failed"); 444 + serializer.serialize_char(val) 445 + } 446 + } 447 + #[derive(Debug, Clone, Copy, serde::Deserialize)] 448 + enum Month { 449 + January, 450 + February, 451 + March, 452 + April, 453 + May, 454 + June, 455 + July, 456 + August, 457 + September, 458 + October, 459 + November, 460 + December, 461 + } 462 + impl Serialize for Month { 463 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 464 + where 465 + S: Serializer, 466 + { 467 + let val = self.to_string().to_lowercase(); 468 + serializer.serialize_str(&val) 469 + } 470 + } 471 + impl std::fmt::Display for Month { 472 + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 473 + write!( 474 + f, 475 + "{}", 476 + match self { 477 + Self::January => "jan", 478 + Self::February => "feb", 479 + Self::March => "mar", 480 + Self::April => "apr", 481 + Self::May => "may", 482 + Self::June => "jun", 483 + Self::July => "jul", 484 + Self::August => "aug", 485 + Self::September => "sep", 486 + Self::October => "oct", 487 + Self::November => "nov", 488 + Self::December => "dec", 489 + } 490 + ) 491 + } 492 + } 493 + /// Profile labels for month+year. 494 + #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] 495 + pub struct ProfileLabel { 496 + year: Year, 497 + month: Month, 498 + } 499 + impl ProfileLabel { 500 + /// Create a new profile label from a datetime. 501 + #[allow(clippy::cognitive_complexity)] 502 + pub fn from_datetime(datetime: Datetime<chrono::Utc>) -> Option<Self> { 503 + Some(Self { 504 + year: match datetime.year() { 505 + 2023 => Year::_2023, 506 + 2024 => Year::_2024, 507 + 2025 => Year::_2025, 508 + _ => { 509 + tracing::debug!("Invalid year"); 510 + return None; 511 + } 512 + }, 513 + month: match datetime.month() { 514 + 1 => Month::January, 515 + 2 => Month::February, 516 + 3 => Month::March, 517 + 4 => Month::April, 518 + 5 => Month::May, 519 + 6 => Month::June, 520 + 7 => Month::July, 521 + 8 => Month::August, 522 + 9 => Month::September, 523 + 10 => Month::October, 524 + 11 => Month::November, 525 + 12 => Month::December, 526 + _ => { 527 + tracing::debug!("Invalid month"); 528 + return None; 529 + } 530 + }, 531 + }) 532 + } 533 + /// Convert a profile label to a string. 534 + pub fn to_label_val(self) -> String { 535 + format!("joined-{}-{}", self.month, self.year) 536 + .to_lowercase() 537 + } 538 + } 539 + /// Profile with optional stats and label. 540 + #[derive(Debug)] 541 + pub struct Profile { 542 + did: String, 543 + /// Stats for a profile. 544 + pub stats: Option<ProfileStats>, 545 + label: Option<String>, 546 + } 547 + impl Profile { 548 + /// Create a new profile, given a DID. 549 + pub fn new(did: &str) -> Self { 550 + Self { 551 + did: { 552 + // if did.starts_with("did:") { 553 + // format!("at://{}{}", did, "/app.bsky.actor.profile/self") 554 + // } else { 555 + did.to_owned() 556 + // } 557 + }, 558 + stats: None, 559 + label: None, 560 + } 561 + } 562 + /// Fetch stats for a profile. 563 + pub async fn determine_stats(&mut self, agent: &mut crate::webrequest::Agent, pool: &sqlx::sqlite::SqlitePool) -> &mut Self { 564 + if let Ok(stats) = ProfileStats::from_at_url(self.did.clone(), agent).await { 565 + self.stats = Some(stats); 566 + } else { 567 + tracing::warn!("Failed to get stats for profile {}", self.did); 568 + } 569 + self.insert_profile_stats(pool).await.expect("Expected to be able to insert profile stats, but failed"); 570 + self 571 + } 572 + /// Determine if stats exist for a profile. 573 + pub async fn determine_stats_exist(&mut self, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Option<&mut Self>, Box<dyn std::error::Error + Sync + Send>> { 574 + if self.stats.is_some() { 575 + return Ok(Some(self)); 576 + } 577 + let profile_stats = sqlx::query!( 578 + r#" 579 + SELECT created_at "created_at: String", follower_count, post_count, checked_at "checked_at: String" FROM profile_stats WHERE did = ? 580 + "#, 581 + self.did 582 + ) 583 + .fetch_one(pool) 584 + .await; 585 + if profile_stats.is_ok() { 586 + let profile_stats = profile_stats.expect("Expected to be able to unwrap a profile_checked_at, but failed"); 587 + let created_at = Datetime::parse_from_rfc3339(profile_stats.created_at.as_str()).expect("Expected to be able to parse a string as a datetime, but failed").to_utc(); 588 + let follower_count = profile_stats.follower_count as i32; 589 + let post_count = profile_stats.post_count as i32; 590 + let checked_at = Datetime::parse_from_rfc3339(profile_stats.checked_at.as_str()).expect("Expected to be able to parse a string as a datetime, but failed").to_utc(); 591 + const TIMEPERIOD: i64 = 60 * 60 * 24 * 7 * 1000 * 4; // 4 weeks in milliseconds 592 + if chrono::Utc::now().timestamp_millis() - checked_at.timestamp_millis() < TIMEPERIOD { 593 + tracing::debug!("Stats exist for: {:?}", self.did); 594 + self.stats = Some(ProfileStats { 595 + follower_count, 596 + post_count, 597 + created_at, 598 + checked_at, 599 + }); 600 + return Ok(Some(self)); 601 + } 602 + tracing::info!("Refetching stats for: {:?}", self.did); 603 + return Ok(None); 604 + } 605 + tracing::info!("Stats do not exist for: {:?}", self.did); 606 + Ok(None) 607 + } 608 + /// Determine the label of a profile. 609 + pub async fn determine_label(&mut self, pool: &sqlx::sqlite::SqlitePool) -> &mut Self { 610 + if self.stats.is_none() { 611 + return self; 612 + } 613 + const MIN_POSTS: i32 = 30; 614 + const SOME_POSTS: i32 = 200; 615 + const MIN_FOLLOWERS: i32 = 400; 616 + const SOME_FOLLOWERS: i32 = 2_500; 617 + let post_count = self.stats.as_ref().expect("Expected stats to exist, but failed").post_count; 618 + let follower_count = self.stats.as_ref().expect("Expected stats to exist, but failed").follower_count; 619 + if (post_count >= MIN_POSTS && follower_count >= MIN_FOLLOWERS) && (post_count >= SOME_POSTS || follower_count >= SOME_FOLLOWERS) 620 + { 621 + match ProfileLabel::from_datetime(self.stats.as_ref().expect("Expected stats to exist, but failed").created_at) { 622 + Some(label) => self.label = Some(label.to_label_val()), 623 + None => { 624 + tracing::debug!("Invalid datetime"); 625 + } 626 + } 627 + } 628 + self.insert_profile_labels(pool).await.expect("Expected to be able to insert profile labels, but failed"); 629 + self 630 + } 631 + /// Determine the label of a profile, and insert it without checking stats reqs. 632 + pub async fn determine_label_agnostic(&mut self, pool: &sqlx::sqlite::SqlitePool) -> &mut Self { 633 + if self.stats.is_none() { 634 + return self; 635 + } 636 + match ProfileLabel::from_datetime(self.stats.as_ref().expect("Expected stats to exist, but failed").created_at) { 637 + Some(label) => self.label = Some(label.to_label_val()), 638 + None => { 639 + tracing::debug!("Invalid datetime"); 640 + } 641 + } 642 + self.insert_profile_labels(pool).await.expect("Expected to be able to insert profile labels, but failed"); 643 + self 644 + } 645 + /// Insert a profile into the database. 646 + pub async fn insert_profile(self, pool: &sqlx::sqlite::SqlitePool) -> Result<Self, sqlx::Error> { 647 + if (sqlx::query(&format!( 648 + "INSERT INTO profile (did) VALUES ('{}')", 649 + self.did 650 + )) 651 + .execute(pool) 652 + .await).is_ok() { 653 + tracing::debug!("Inserted profile {:?}", self.did); 654 + } else { 655 + tracing::debug!("Duplicate profile: {:?}", self.did); 656 + } 657 + Ok(self) 658 + } 659 + /// Insert profile stats into the database. 660 + pub async fn insert_profile_stats( 661 + &self, 662 + pool: &sqlx::sqlite::SqlitePool, 663 + ) -> Result<(), sqlx::Error> { 664 + if self.stats.is_none() { 665 + return Ok(()); 666 + } 667 + // if sqlx::query!( 668 + // r#"SELECT did "did: String" FROM profile_stats WHERE did = ? LIMIT 1"#, 669 + // self.did 670 + // ) 671 + // .fetch_one(pool) 672 + // .await.is_ok() { 673 + // tracing::debug!("Stats already exist for {:?}", self.did); 674 + // return Ok(()); 675 + // } 676 + let created_at = self.stats.as_ref().expect("Expected stats to exist, but failed").created_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true); 677 + let follower_count = self.stats.as_ref().expect("Expected stats to exist, but failed").follower_count; 678 + let post_count = self.stats.as_ref().expect("Expected stats to exist, but failed").post_count; 679 + let checked_at = self.stats.as_ref().expect("Expected stats to exist, but failed").checked_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true); 680 + _ = sqlx::query!(r#" 681 + INSERT INTO profile_stats (did, created_at, follower_count, post_count, checked_at) 682 + VALUES (?, ?, ?, ?, ?) 683 + ON CONFLICT(did) DO UPDATE SET 684 + created_at = ?, 685 + follower_count = ?, 686 + post_count = ?, 687 + checked_at = ? 688 + "#, 689 + self.did, 690 + created_at, 691 + follower_count, 692 + post_count, 693 + checked_at, 694 + created_at, 695 + follower_count, 696 + post_count, 697 + checked_at, 698 + ) 699 + .execute(pool).await.expect("Expected to be able to insert profile stats, but failed"); 700 + tracing::info!("Inserted profile stats for {:?} with {:?} followers", self.did, self.stats.as_ref().expect("Expected stats to exist, but failed").follower_count); 701 + Ok(()) 702 + } 703 + /// Negate a profile label. 704 + pub async fn negate_label( 705 + &mut self, 706 + pool: &sqlx::sqlite::SqlitePool, 707 + ) -> Result<(), sqlx::Error> { 708 + if self.stats.is_none() { 709 + tracing::warn!("No stats for {:?}", self.did); 710 + return Ok(()); 711 + } 712 + match ProfileLabel::from_datetime(self.stats.as_ref().expect("Expected stats to exist, but failed").created_at) { 713 + Some(label) => self.label = Some(label.to_label_val()), 714 + None => { 715 + tracing::debug!("Invalid datetime"); 716 + } 717 + } 718 + let label = self.label.as_ref().expect("Expected label to exist, but failed"); 719 + let uri = self.did.as_str(); 720 + let val: &str = label.as_str(); 721 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 722 + let self_did = dotenvy::var("SELF_DID").expect("Expected to be able to get the SELF_DID from the environment, but failed"); 723 + let src = Did::new(self_did).expect("Expected to be able to create a valid DID but failed"); 724 + let mut label_response: AssignedLabelResponse = AssignedLabelResponse::generate(src, self.did.clone(), val.to_owned()); 725 + label_response.neg = true; 726 + label_response = label_response.sign(); 727 + let sig_enum = label_response.sig.expect("Expected a signature, but failed"); 728 + if let SignatureEnum::Bytes(sig) = sig_enum { 729 + let sig = sig.as_vec(); 730 + _ = sqlx::query!( 731 + r#"INSERT INTO profile_labels (uri, val, neg, cts, sig) VALUES (?, ?, ?, ?, ?)"#, 732 + uri, 733 + val, 734 + label_response.neg, 735 + label_response.cts, 736 + sig, 737 + ) 738 + .execute(pool) 739 + .await?; 740 + } 741 + tracing::info!("Negated profile label for {:?} with {:?}", self.did, self.label.as_ref().expect("Expected label to exist, but failed")); 742 + Ok(()) 743 + } 744 + /// Insert profile labels into the database. 745 + async fn insert_profile_labels( 746 + &self, 747 + pool: &sqlx::sqlite::SqlitePool, 748 + ) -> Result<(), sqlx::Error> { 749 + if self.label.is_none() { 750 + return Ok(()); 751 + } 752 + if sqlx::query!( 753 + r#"SELECT seq FROM profile_labels WHERE uri = ? LIMIT 1"#, 754 + self.did 755 + ) 756 + .fetch_one(pool) 757 + .await.is_ok() { 758 + tracing::debug!("Label already exists for {:?}", self.did); 759 + return Ok(()); 760 + } 761 + let label = self.label.as_ref().expect("Expected label to exist, but failed"); 762 + let uri = self.did.as_str(); 763 + let val: &str = label.as_str(); 764 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 765 + let self_did = dotenvy::var("SELF_DID").expect("Expected to be able to get the SELF_DID from the environment, but failed"); 766 + let src = Did::new(self_did).expect("Expected to be able to create a valid DID but failed"); 767 + let mut label_response: AssignedLabelResponse = AssignedLabelResponse::generate(src, self.did.clone(), val.to_owned()); 768 + label_response = label_response.sign(); 769 + let sig_enum = label_response.sig.expect("Expected a signature, but failed"); 770 + if let SignatureEnum::Bytes(sig) = sig_enum { 771 + let sig = sig.as_vec(); 772 + let result = sqlx::query!( 773 + r#"INSERT INTO profile_labels (uri, val, cts, sig) VALUES (?, ?, ?, ?)"#, 774 + uri, 775 + val, 776 + label_response.cts, 777 + sig, 778 + ) 779 + .execute(pool) 780 + .await; 781 + if result.is_ok() { 782 + tracing::info!("Inserted profile label for {:?} with {:?}", self.did, self.label.as_ref().expect("Expected label to exist, but failed")); 783 + } else { 784 + tracing::debug!("Duplicate profile label for {:?}", self.did); 785 + } 786 + return Ok(()); 787 + } 788 + tracing::warn!("Failed to insert profile label for {:?}", self.did); 789 + Ok(()) 790 + } 791 + /// Remove label from profile_labels. 792 + /// Used when a label needs to be regenerated. 793 + pub async fn remove_label( 794 + pool: &sqlx::sqlite::SqlitePool, 795 + seq: i64, 796 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 797 + tracing::debug!("Removing label with seq: {:?}", seq); 798 + _ = sqlx::query!( 799 + r#"DELETE FROM profile_labels WHERE seq = ?"#, 800 + seq, 801 + ) 802 + .execute(pool) 803 + .await.expect("Expected to be able to delete a label, but failed."); 804 + Ok(()) 805 + } 806 + }
+340
src/webrequest.rs
··· 1 + //! Requests to the atproto API. 2 + use std::env; 3 + use std::fs; 4 + use std::sync::Arc; 5 + 6 + use base64::Engine; 7 + use reqwest::Client; 8 + use tokio::sync::Mutex; 9 + 10 + use crate::types::{LabelsVecWithSeq, RetrievedLabelResponse, SignatureBytes}; 11 + enum ApiEndpoint { 12 + Authorized, 13 + Public, 14 + } 15 + /// Agent for interactions with atproto. 16 + #[derive(Clone)] 17 + pub struct Agent { 18 + /// The access JWT. 19 + pub access_jwt: Arc<Mutex<String>>, 20 + /// The refresh JWT. 21 + pub refresh_jwt: Arc<Mutex<String>>, 22 + /// The reqwest client. 23 + pub client: Client, 24 + /// The DID of the labeler. 25 + pub self_did: Arc<String>, 26 + } 27 + impl Default for Agent { 28 + fn default() -> Self { 29 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 30 + Self { 31 + access_jwt: Arc::new(Mutex::new(env::var("ACCESS_JWT").expect("ACCESS_JWT must be set"))), 32 + refresh_jwt: Arc::new(Mutex::new(env::var("REFRESH_JWT").expect("REFRESH_JWT must be set"))), 33 + client: Client::new(), 34 + self_did: Arc::new(env::var("SELF_DID").expect("SELF_DID must be set")), 35 + } 36 + } 37 + } 38 + impl Agent { 39 + /// The base URL of the atproto API's XRPC endpoint. 40 + /// Rate limit: 3_000 per 5 minutes 41 + const AUTH_URL: &'static str = "https://bsky.social/xrpc/"; 42 + const PUBLIC_URL: &'static str = "https://public.api.bsky.app/xrpc/"; 43 + async fn client_get( 44 + &self, 45 + path: &str, 46 + parameters: &[(&str, &str)], 47 + api_endpoint: &ApiEndpoint, 48 + ) -> reqwest::Response { 49 + self.client 50 + .get(format!("{}{}", match api_endpoint { 51 + ApiEndpoint::Authorized => Self::AUTH_URL, 52 + ApiEndpoint::Public => Self::PUBLIC_URL, 53 + }, &path)) 54 + .header("Content-Type", "application/json") 55 + .header("Authorization", format!("Bearer {}", self.access_jwt.lock().await)) 56 + .header("atproto-accept-labelers", self.self_did.as_str()) 57 + .query(parameters) 58 + .send() 59 + .await.expect("Expected to be able to send request, but failed.") 60 + } 61 + async fn client_refresh(&self) { 62 + tracing::warn!("Token expired, refreshing"); 63 + let response = self.client 64 + .post(format!( 65 + "{}{}", 66 + Self::AUTH_URL, 67 + "com.atproto.server.refreshSession" 68 + )) 69 + .header("Content-Type", "application/json") 70 + .header("Authorization", format!("Bearer {}", self.refresh_jwt.lock().await)) 71 + .header("atproto-accept-labelers", self.self_did.as_str()) 72 + .send() 73 + .await.expect("Expected to be able to send request, but failed."); 74 + let json = response.json::<serde_json::Value>().await.expect("Expected to be able to read response as JSON, but failed."); 75 + if let Some(error) = json["error"].as_str() { 76 + match error { 77 + "InvalidRequest" => { 78 + tracing::warn!("Invalid request"); 79 + return; 80 + }, 81 + "ExpiredToken" => { 82 + tracing::warn!("Token expired"); 83 + return; 84 + }, 85 + "AccountDeactivated" => { 86 + tracing::warn!("Account deactivated"); 87 + return; 88 + }, 89 + "AccountTakedown" => { 90 + tracing::warn!("Account has been suspended (Takedown)"); 91 + return; 92 + }, 93 + _ => { 94 + tracing::warn!("Unknown error from HTTP response: {:?}", json); 95 + return; 96 + } 97 + } 98 + } 99 + *self.refresh_jwt.lock().await = json["refreshJwt"].as_str().expect("Expected to be able to read refreshJwt as str, but failed.").to_owned(); 100 + *self.access_jwt.lock().await = json["accessJwt"].as_str().expect("Expected to be able to read accessJwt as str, but failed.").to_owned(); 101 + let new_env = format!( 102 + "ACCESS_JWT={}\nREFRESH_JWT={}\n", 103 + self.access_jwt.lock().await, self.refresh_jwt.lock().await 104 + ); 105 + fs::write(".env", new_env).expect("Failed to write to .env"); 106 + tracing::info!("Token refreshed"); 107 + } 108 + /// Get a JSON response from the atproto API. Used internal to this struct. 109 + async fn get( 110 + &self, 111 + path: &str, 112 + parameters: &[(&str, &str)], 113 + api_endpoint: ApiEndpoint, 114 + ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> { 115 + let response = self.client_get(path, parameters, &api_endpoint).await; 116 + if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { 117 + tracing::warn!("Rate limited, sleeping for 5 minutes"); 118 + tracing::warn!("We were working on {} with parameters {:?}", path, parameters); 119 + tokio::time::sleep(std::time::Duration::from_secs(305)).await; // 5 minutes and 5 seconds 120 + let response = self.client_get(path, parameters, &api_endpoint).await; 121 + return Ok(response.json::<serde_json::Value>().await.expect("Expected to be able to read response as JSON, but failed.")); 122 + } 123 + if response.status() == reqwest::StatusCode::BAD_REQUEST { 124 + let json = &response.json::<serde_json::Value>().await.expect("Expected to be able to read response as JSON, but failed."); 125 + match json["error"].as_str().expect("Expected to be able to read error as str, but failed.") { 126 + "ExpiredToken" => { 127 + self.client_refresh().await; 128 + let response = self.client_get(path, parameters, &api_endpoint).await; 129 + return Ok(response.json::<serde_json::Value>().await.expect("Expected to be able to read response as JSON, but failed.")); 130 + }, 131 + "AccountDeactivated" => { 132 + tracing::warn!("Account deactivated"); 133 + return Err(Box::new(std::io::Error::new( 134 + std::io::ErrorKind::Other, 135 + "Account deactivated", 136 + ))); 137 + }, 138 + "AccountTakedown" => { 139 + tracing::warn!("Account has been suspended (Takedown)"); 140 + return Err(Box::new(std::io::Error::new( 141 + std::io::ErrorKind::Other, 142 + "Account deactivated", 143 + ))); 144 + }, 145 + "InvalidRequest" => { 146 + // Check if the message is "Profile not found" 147 + if json["message"].as_str().expect("Expected to be able to read message as str, but failed.") == "Profile not found" { 148 + tracing::warn!("Profile not found"); 149 + return Err(Box::new(std::io::Error::new( 150 + std::io::ErrorKind::NotFound, 151 + "Profile not found", 152 + ))); 153 + } 154 + tracing::warn!("Unknown invalid request: {:?}", json); 155 + return Err(Box::new(std::io::Error::new( 156 + std::io::ErrorKind::Other, 157 + "Unknown invalid request", 158 + ))); 159 + }, 160 + _ => { 161 + tracing::warn!("Unknown error from HTTP response: {:?}", json); 162 + return Err(Box::new(std::io::Error::new( 163 + std::io::ErrorKind::Other, 164 + "Unknown bad request", 165 + ))); 166 + } 167 + }; 168 + } 169 + if response.status() != reqwest::StatusCode::OK { 170 + return Err(Box::new(std::io::Error::new( 171 + std::io::ErrorKind::Other, 172 + "Unknown HTTP error", 173 + ))); 174 + } 175 + let json = response.json::<serde_json::Value>().await.expect("Expected to be able to read response as JSON, but failed."); 176 + Ok(json) 177 + } 178 + /// Get a profile from the atproto API. 179 + pub async fn get_profile( 180 + &mut self, 181 + profile_id: &str, 182 + ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> { 183 + let path = "app.bsky.actor.getProfile"; 184 + let parameters = [("actor", profile_id)]; 185 + self.get(path, &parameters, ApiEndpoint::Public).await 186 + } 187 + /// Get multiple profiles. 188 + pub async fn get_profiles( 189 + &mut self, 190 + profile_ids: &[String], 191 + ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> { 192 + let path = "app.bsky.actor.getProfiles"; 193 + let mut parameters = Vec::new(); 194 + for profile_id in profile_ids { 195 + parameters.push(("actors", profile_id.as_str())); 196 + } 197 + self.get(path, parameters.as_slice(), ApiEndpoint::Authorized).await 198 + } 199 + /// Check if a list of profiles has a label from us. 200 + pub async fn check_profiles( 201 + &mut self, 202 + profile_ids: &[(String, i64)], 203 + ) -> Result<Vec<(bool, (String, i64))>, Box<dyn std::error::Error + Send + Sync>> { 204 + let mut found_labels: Vec<(bool, (String, i64))> = Vec::new(); 205 + let profile_ids_uris = profile_ids.iter().map(|(profile_id, _)| profile_id.clone()).collect::<Vec<String>>(); 206 + let profile_ids_seqs = profile_ids.iter().map(|(_, seq)| seq).collect::<Vec<&i64>>(); 207 + let profiles = self.get_profiles(profile_ids_uris.as_slice()).await?; 208 + let profiles_array = profiles["profiles"].as_array(); 209 + if profiles_array.is_none() { 210 + tracing::warn!("No profiles json found for profiles: {:?}", profiles); 211 + return Ok(vec![]); 212 + } 213 + for profile in profiles_array.unwrap_or_else(|| panic!("Expected to be able to read profiles as array, but failed. Profiles: {:?}", profiles)) { 214 + let labels = &profile["labels"]; 215 + let mut found = false; 216 + let label_array = labels.as_array(); 217 + if label_array.is_none() { 218 + tracing::warn!("No labels json found for profile: {:?}", profile); 219 + continue; 220 + } 221 + let did = profile["did"].as_str().expect("Expected to be able to read did as str, but failed."); 222 + let seq = profile_ids_seqs[profile_ids_uris.iter().position(|x| x == did).expect("Expected to be able to find the index of the uri.")]; 223 + for label in label_array.unwrap_or_else(|| panic!("Expected to be able to read labels as array, but failed. Profile: {:?}", profile)) { 224 + if label["src"].as_str().expect("Expected to be able to read src as str, but failed.") == self.self_did.as_str() { 225 + found = true; 226 + break; 227 + } 228 + } 229 + found_labels.push((found, (did.to_owned(), *seq))); 230 + } 231 + Ok(found_labels) 232 + } 233 + /// After getting a profile, check the labels on it, and see if one from us ("src:") is there. 234 + pub async fn check_profile( 235 + &mut self, 236 + profile_did: &str, 237 + ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> { 238 + let profile = self.get_profile(profile_did).await?; 239 + let labels = &profile["labels"]; 240 + let label_array = labels.as_array(); 241 + if label_array.is_none() { 242 + tracing::warn!("No labels json found for profile: {:?}", profile); 243 + return Ok(false); 244 + } 245 + for label in label_array.unwrap_or_else(|| panic!("Expected to be able to read labels as array, but failed. Profile: {:?}", profile)) { 246 + if label["src"].as_str().expect("Expected to be able to read src as str, but failed.") == self.self_did.as_str() { 247 + return Ok(true); 248 + } 249 + } 250 + Ok(false) 251 + } 252 + /// Get a label from the provided URL, then validate the signature. 253 + pub async fn get_label_and_validate( 254 + &self, 255 + url: &str, 256 + ) -> Result<(), Box<dyn std::error::Error>> { 257 + tracing::debug!("Getting label from {}", url); 258 + let response = reqwest::get(url).await.expect("Expected to be able to get response, but failed."); 259 + tracing::debug!("Response: {:?}", response); 260 + let response_json = response.json::<serde_json::Value>().await.expect("Expected to be able to read response as JSON, but failed."); 261 + tracing::debug!("Response JSON: {:?}", response_json); 262 + let sig = &response_json["labels"][0]["sig"]; 263 + tracing::debug!("Signature: {:?}", sig); 264 + let retrieved_label = RetrievedLabelResponse { 265 + // id: response_json["labels"][0]["id"].as_u64().unwrap(), 266 + cts: response_json["labels"][0]["cts"].as_str().expect("Expected to be able to read cts as str, but failed.").to_owned(), 267 + neg: response_json["labels"][0]["neg"].as_str() == Some("true"), 268 + src: response_json["labels"][0]["src"].as_str().expect("Expected to be able to read src as str, but failed.").to_owned().parse().expect("Failed to parse DID"), 269 + uri: response_json["labels"][0]["uri"].as_str().expect("Expected to be able to read uri as str, but failed.").to_owned().parse().expect("Failed to parse URI"), 270 + val: response_json["labels"][0]["val"].as_str().expect("Expected to be able to read val as str, but failed.").to_owned(), 271 + ver: response_json["labels"][0]["ver"].as_u64().expect("Expected to be able to read ver as u64, but failed."), 272 + }; 273 + let crypto = crate::crypto::Crypto::new(); 274 + let pub_key = "zQ3shreqyXEdouQeEQSFKfoSEN5eig74BXuqQyTaiE9uzADqZ"; 275 + let sig_string = sig["$bytes"].as_str().expect("Expected to be able to read sig as str, but failed."); 276 + if crypto.validate(retrieved_label, sig_string, pub_key) { 277 + tracing::info!("Valid signature"); 278 + Ok(()) 279 + } else { 280 + tracing::info!("Invalid signature"); 281 + Err(Box::new(std::io::Error::new( 282 + std::io::ErrorKind::Other, 283 + "Invalid signature", 284 + ))) 285 + } 286 + } 287 + /// Get a label from a websocket URL, then validate the signature. 288 + /// Similar to what's done in webserve.rs, but in reverse, we'll need to decode the message. 289 + pub async fn get_label_and_validate_ws( 290 + &self, 291 + // url: &str, 292 + ) -> Result<(), Box<dyn std::error::Error>> { 293 + // For now, use this mock response, represented in base64: 294 + let response = "omF0ZyNsYWJlbHNib3ABomNzZXEYG2ZsYWJlbHOBp2NjdHN4GzIwMjUtMDItMDlUMDM6MjU6MjcuOTI4MDIzWmNuZWf0Y3NpZ1hAXLIRXAG5mF5bCWWCwEhbYvC8YYVP9fWwbVVL6IBXXlIrZ6sr6MQ4DfNdpGhwRWawA4Mq44HlEDsJ7OvcGsDCDWNzcmN4IGRpZDpwbGM6bTZhZHB0bjYyZGNhaGZhcTM0dGNlM2o1Y3VyaXggZGlkOnBsYzptNmFkcHRuNjJkY2FoZmFxMzR0Y2UzajVjdmFsbmpvaW5lZC0yMDI1LTAyY3ZlcgE="; 295 + tracing::debug!("Response: {:?}", response); 296 + let response_bytes = base64::engine::GeneralPurpose::new( 297 + &base64::alphabet::STANDARD, 298 + base64::engine::general_purpose::PAD).decode(response).expect("Expected to be able to decode base64 response."); 299 + tracing::debug!("Response bytes: {:?}", response_bytes); 300 + let reponse_bytes_in_hex = hex::encode(&response_bytes); 301 + tracing::debug!("Response bytes in hex: {:?}", reponse_bytes_in_hex); 302 + let response_0 = &response_bytes[0..response_bytes.iter().position(|&r| r == 0x01).expect("Expected to find 0x01 in response bytes.")]; 303 + let response_1 = &response_bytes[response_bytes.iter().position(|&r| r == 0x01).expect("Expected to find 0x01 in response bytes.") + 1..]; 304 + tracing::debug!("Response 0: {:?}", hex::encode(response_0)); 305 + tracing::debug!("Response 1: {:?}", hex::encode(response_1)); 306 + let response_cbor: LabelsVecWithSeq = serde_cbor::from_slice(response_1).expect("Expected to be able to deserialize response 1 as LabelsVecWithSeq, but failed."); 307 + tracing::debug!("Response CBOR: {:?}", response_cbor); 308 + let unsigned_response = RetrievedLabelResponse { 309 + cts: response_cbor.labels[0].cts.clone(), 310 + neg: response_cbor.labels[0].neg, 311 + src: response_cbor.labels[0].src.clone(), 312 + uri: response_cbor.labels[0].uri.clone(), 313 + val: response_cbor.labels[0].val.clone(), 314 + ver: response_cbor.labels[0].ver, 315 + }; 316 + let sig_base64 = SignatureBytes::from_bytes(response_cbor.labels[0].sig).as_base64(); 317 + tracing::debug!("Retrieved label: {:?}", response_cbor); 318 + let crypto = crate::crypto::Crypto::new(); 319 + let public_key = "zQ3shreqyXEdouQeEQSFKfoSEN5eig74BXuqQyTaiE9uzADqZ"; 320 + if crypto.validate(unsigned_response, &sig_base64, public_key) { 321 + tracing::info!("Valid signature"); 322 + Ok(()) 323 + } else { 324 + tracing::info!("Invalid signature"); 325 + Err(Box::new(std::io::Error::new( 326 + std::io::ErrorKind::Other, 327 + "Invalid signature", 328 + ))) 329 + } 330 + } 331 + /// getLikes 332 + pub async fn get_likes( 333 + &mut self, 334 + uri: &str, 335 + ) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error + Send + Sync>> { 336 + let path = "app.bsky.feed.getLikes"; 337 + let parameters = [("uri", uri)]; 338 + self.get(path, &parameters, ApiEndpoint::Public).await.map(|response| response["likes"].as_array().expect("Expected to be able to read likes as array, but failed.").to_owned()) 339 + } 340 + }
+488
src/webserve.rs
··· 1 + //! Serving requests as a labeler. 2 + 3 + use axum::{ 4 + body::Bytes, 5 + extract::ws::{Message, Utf8Bytes, WebSocket, WebSocketUpgrade}, 6 + response::IntoResponse, 7 + routing::any, 8 + Router, 9 + }; 10 + use axum_extra::TypedHeader; 11 + use headers::{Header, HeaderName, HeaderValue}; 12 + use sqlx::{sqlite::{SqliteConnectOptions, SqliteJournalMode}, SqlitePool}; 13 + use std::str::FromStr; 14 + use std::ops::ControlFlow; 15 + use std::net::SocketAddr; 16 + use axum::extract::connect_info::ConnectInfo; 17 + use axum::extract::ws::CloseFrame; 18 + use futures::{sink::SinkExt, stream::StreamExt}; 19 + use axum::extract::Query; 20 + use axum::{Json, http::StatusCode, routing::get}; 21 + use serde::Deserialize; 22 + use tower_http::decompression::RequestDecompressionLayer; 23 + use tower_http::compression::CompressionLayer; 24 + 25 + use crate::{types::{AssignedLabelResponse, AssignedLabelResponseWrapper, SignatureBytes, SignatureEnum, SubscribeLabelsLabels, UriParams}, webrequest::Agent}; 26 + 27 + 28 + /// Launch the web server to respond to label inquiries. 29 + #[tracing::instrument] 30 + pub async fn main_webserve() { 31 + let app = Router::new() 32 + .route("/xrpc/com.atproto.label.subscribeLabels", any(subscribe_labels)) 33 + .route("/xrpc/com.atproto.label.queryLabels", get(query_labels)) 34 + .route("/xrpc/app.bsky.actor.getProfile", get(get_profile)) 35 + .layer(RequestDecompressionLayer::new()) 36 + .layer(CompressionLayer::new().deflate(true)); 37 + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") 38 + .await 39 + .expect("Expected to bind to 0.0.0.0:3000 but failed."); 40 + tracing::debug!("listening on {}", listener.local_addr().expect("Expected to get local address but failed.")); 41 + axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.expect("Expected to be able to use axum::serve but failed."); 42 + } 43 + 44 + /// Querys by DID. \ 45 + async fn query_labels(Query(params): Query<UriParams>) -> impl IntoResponse { 46 + tracing::debug!("Querying labels: {:?}", params.uriPatterns); 47 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 48 + let self_did = dotenvy::var("SELF_DID").expect("Expected to be able to get the SELF_DID from the environment, but failed"); 49 + let src = jetstream_oxide::exports::Did::new(self_did).expect("Expected to be able to create a valid DID but failed"); 50 + if let Some(uri_patterns) = params.uriPatterns { 51 + let pool_opts = SqliteConnectOptions::from_str("sqlite://prod.db?mode=ro").expect("Expected to be able to configure the database, but failed.") 52 + .journal_mode(SqliteJournalMode::Wal) 53 + .read_only(true); 54 + let pool = SqlitePool::connect_with(pool_opts).await.expect("Expected to be able to connect to the database at sqlite://prod.db but failed."); 55 + let pattern = uri_patterns.replace("%", "").replace("_", "\\_"); // .replaceAll(/%/g, "").replaceAll(/_/g, "\\_"); 56 + let star_index = pattern.find('*'); 57 + let limit = params.limit.unwrap_or(50); 58 + let cursor = params.cursor.unwrap_or_else(|| "0".to_owned()); 59 + if let Some(star_index) = star_index { 60 + if star_index != pattern.len() - 1 { 61 + return (StatusCode::BAD_REQUEST, Json(AssignedLabelResponseWrapper { 62 + cursor: "0".to_owned(), // TODO: Other servers don't respond with a cursor in this scenario. 63 + labels: Vec::<AssignedLabelResponse>::new() 64 + })); 65 + } 66 + let labels = sqlx::query!( 67 + r#" 68 + SELECT seq, uri "uri: String", val "val: String", neg, cts "cts: String", sig 69 + FROM profile_labels 70 + WHERE seq > ? 71 + LIMIT ? 72 + "#, 73 + cursor, 74 + limit 75 + ) 76 + .fetch_all(&pool) 77 + .await 78 + .expect("Expected to be able to fetch all labels from the database but failed."); 79 + let smallest_cursor = labels.iter().map(|label| label.seq).min().unwrap_or(0); 80 + return (StatusCode::OK, Json(AssignedLabelResponseWrapper { 81 + cursor: smallest_cursor.to_string(), 82 + labels: labels 83 + .iter() 84 + .map(|label| { 85 + AssignedLabelResponse::reconstruct( 86 + src.to_owned(), 87 + label.uri.clone(), 88 + label.val.clone(), 89 + label.neg.unwrap_or(false), 90 + label.cts.clone(), 91 + SignatureEnum::Json(SignatureBytes::from_vec(label.sig.clone()).as_json_object()), 92 + ) 93 + }) 94 + .collect(), 95 + })); 96 + } 97 + let labels = sqlx::query!( 98 + r#" 99 + SELECT seq "seq: i64", uri "uri: String", val "val: String", neg, cts "cts: String", sig 100 + FROM profile_labels WHERE uri = ? AND seq > ? LIMIT ? 101 + "#, 102 + uri_patterns, 103 + cursor, 104 + limit 105 + ) 106 + .fetch_all(&pool) 107 + .await 108 + .expect("Expected to be able to fetch all missing labels from the database but failed."); 109 + let largest_cursor = labels.iter().map(|label| label.seq).max().unwrap_or(0); 110 + return (StatusCode::OK, Json(AssignedLabelResponseWrapper { 111 + cursor: largest_cursor.to_string(), 112 + labels: labels 113 + .iter() 114 + .map(|label| { 115 + AssignedLabelResponse::reconstruct( 116 + src.to_owned(), 117 + label.uri.clone(), 118 + label.val.clone(), 119 + label.neg.unwrap_or(false), 120 + label.cts.clone(), 121 + SignatureEnum::Json(SignatureBytes::from_vec(label.sig.clone()).as_json_object()), 122 + ) 123 + }) 124 + .collect(), 125 + })); 126 + } 127 + ( 128 + StatusCode::OK, 129 + Json(AssignedLabelResponseWrapper { 130 + cursor: "0".to_owned(), 131 + labels: Vec::<AssignedLabelResponse>::new() 132 + }), 133 + ) 134 + } 135 + /// Querys by profile name. 136 + async fn get_profile(Query(params): Query<UriParams>) -> impl IntoResponse { 137 + if let Some(actor) = &params.actor { 138 + let mut agent = Agent::default(); 139 + if let Ok(profile) = agent.get_profile(actor).await { 140 + return (StatusCode::OK, Json(profile)); 141 + } 142 + } 143 + (StatusCode::OK, Json(serde_json::json!({}))) 144 + } 145 + 146 + /// Query parameters for subscribing to labels. 147 + #[derive(Deserialize)] 148 + struct SubscribeLabelsQueryParams { 149 + /// The last known event seq number to backfill from. 150 + cursor: Option<u64>, 151 + } 152 + 153 + 154 + #[derive(Debug)] 155 + struct XForwardedFor(String); 156 + 157 + impl Header for XForwardedFor { 158 + fn name() -> &'static HeaderName { 159 + &http::header::FORWARDED 160 + } 161 + 162 + fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error> 163 + where 164 + I: Iterator<Item = &'i HeaderValue>, 165 + { 166 + let value = values 167 + .next() 168 + .ok_or_else(headers::Error::invalid)?; 169 + 170 + // We are only interested in the first IP address in the list. 171 + let ip = value 172 + .to_str() 173 + .map_err(|_| headers::Error::invalid())? 174 + .split(',') 175 + .next() 176 + .ok_or_else(headers::Error::invalid)?; 177 + 178 + Ok(Self(ip.to_owned())) 179 + } 180 + 181 + fn encode<E>(&self, values: &mut E) 182 + where 183 + E: Extend<HeaderValue>, 184 + { 185 + let value = HeaderValue::from_str(&self.0).expect("Expected to be able to convert the X-Forwarded-For header to a string but failed."); 186 + values.extend(std::iter::once(value)); 187 + } 188 + } 189 + 190 + /// The handler for the HTTP request. 191 + /// 192 + /// This gets called when the HTTP request lands at the start 193 + /// of websocket negotiation. After this completes, the actual switching from HTTP to 194 + /// websocket protocol will occur. 195 + /// This is the last point where we can extract TCP/IP metadata such as IP address of the client 196 + /// as well as things from HTTP headers such as user-agent of the browser etc. 197 + async fn subscribe_labels( 198 + ws: WebSocketUpgrade, 199 + user_agent: Option<TypedHeader<headers::UserAgent>>, 200 + x_forwarded_for: Option<TypedHeader<XForwardedFor>>, 201 + ConnectInfo(connection_address): ConnectInfo<SocketAddr>, 202 + Query(params): Query<SubscribeLabelsQueryParams>, 203 + ) -> impl IntoResponse { 204 + let user_agent = if let Some(TypedHeader(user_agent)) = user_agent { 205 + user_agent.to_string() 206 + } else { 207 + String::from("Unknown browser") 208 + }; 209 + // Check X-Forwarded-For header to get the apparent IP address of the client 210 + // TODO: This header can be spoofed, and should only be trusted from a trusted proxy. 211 + let apparent_ip = if let Some(TypedHeader(x_forwarded_for)) = x_forwarded_for { 212 + SocketAddr::new(x_forwarded_for.0.parse().expect("Expected to be able to parse the X-Forwarded-For header as a socket address but failed."), 213 + connection_address.port()) 214 + } else { 215 + connection_address 216 + }; 217 + tracing::debug!("`{user_agent}` at {apparent_ip} connected."); 218 + let pool_opts = SqliteConnectOptions::from_str("sqlite://prod.db?mode=ro").expect("Expected to be able to configure the database, but failed.") 219 + .journal_mode(SqliteJournalMode::Wal) 220 + .read_only(true); 221 + let pool = SqlitePool::connect_with(pool_opts).await.expect("Expected to be able to connect to the database at sqlite://prod.db but failed."); 222 + let cursor = params.cursor.unwrap_or( 223 + get_current_cursor_count(&pool) 224 + .await.expect("Expected to be able to get the current cursor count but failed.") 225 + .try_into().expect("Expected to be able to convert the current cursor count to a u64 but failed.") 226 + ) as i64; 227 + // finalize the upgrade process by returning upgrade callback. 228 + // we can customize the callback by sending additional info such as address. 229 + ws.on_upgrade(move |socket| handle_socket(socket, apparent_ip, cursor, pool)) 230 + } 231 + 232 + async fn get_current_cursor_count( 233 + pool: &SqlitePool, 234 + ) -> Result<i64, sqlx::Error> { 235 + let current_cursor_count = sqlx::query!( 236 + r#" 237 + SELECT seq FROM profile_labels ORDER BY seq DESC LIMIT 1 238 + "# 239 + ) 240 + .fetch_one(pool) 241 + .await?; 242 + Ok(current_cursor_count.seq) 243 + } 244 + 245 + /// Actual websocket statemachine (one will be spawned per connection) 246 + async fn handle_socket(socket: WebSocket, who: SocketAddr, cursor: i64, pool: SqlitePool) { 247 + let _ = websocket_context(socket, who, cursor, pool).await; 248 + // returning from the handler closes the websocket connection 249 + tracing::debug!("Websocket context {who} destroyed"); 250 + } 251 + 252 + /// Get all missed messages, based on cursor. 253 + async fn get_missed_messages( 254 + pool: &SqlitePool, 255 + cursor: i64, 256 + ) -> Result<Vec<SubscribeLabelsLabels>, sqlx::Error> { 257 + let missed_messages = sqlx::query!( 258 + r#" 259 + SELECT seq, uri "uri: String", val "val: String", neg "neg: bool", cts "cts: String", sig 260 + FROM profile_labels WHERE seq > ? 261 + "#, 262 + cursor 263 + ) 264 + .fetch_all(pool) 265 + .await?; 266 + drop(dotenvy::dotenv().expect("Failed to load .env file")); 267 + let self_did = dotenvy::var("SELF_DID").expect("Expected to be able to get the SELF_DID from the environment, but failed"); 268 + let src = jetstream_oxide::exports::Did::new(self_did).expect("Expected to be able to create a valid DID but failed"); 269 + Ok(missed_messages 270 + .iter() 271 + .map(|label| { 272 + SubscribeLabelsLabels { 273 + seq: label.seq, 274 + labels: vec![AssignedLabelResponse::reconstruct( 275 + src.to_owned(), 276 + label.uri.clone(), 277 + label.val.clone(), 278 + label.neg.unwrap_or(false), 279 + label.cts.clone(), 280 + SignatureEnum::Bytes(SignatureBytes::from_vec(label.sig.clone())), 281 + )], 282 + } 283 + }) 284 + .collect()) 285 + } 286 + 287 + async fn websocket_context(mut socket: WebSocket, who: SocketAddr, mut cursor: i64, pool: SqlitePool) -> ControlFlow<()> { 288 + ws_send( 289 + &mut socket, 290 + who, 291 + Message::Ping(Bytes::from_static(&[1, 2, 3])) 292 + ).await?; 293 + tracing::info!("{who} connected with cursor {cursor}"); 294 + let current_cursor_count = get_current_cursor_count(&pool).await.unwrap_or_default(); 295 + tracing::debug!("Current cursor count: {current_cursor_count}"); 296 + if cursor < current_cursor_count { 297 + let missed_messages = get_missed_messages(&pool, cursor).await.expect("Expected to be able to get missed messages but failed."); 298 + for message in missed_messages { 299 + tracing::info!("Sending missed message to {who}: {:?}", message); 300 + let message_header: Vec<u8> = Bytes::from_static(b"\xa2atg#labelsbop\x01").into(); 301 + let message_body = serde_cbor::to_vec(&message).expect("Expected to be able to serialize message to CBOR but failed."); 302 + let message_combined = [message_header, message_body].concat(); 303 + let message_finished = Message::Binary(message_combined.into()); 304 + ws_send( 305 + &mut socket, 306 + who, 307 + message_finished 308 + ).await?; 309 + } 310 + cursor = current_cursor_count; 311 + } 312 + // By splitting socket we can send and receive at the same time. In this example we will send 313 + // unsolicited messages to client based on some sort of server's internal event (i.e .timer). 314 + let (mut sender, mut receiver) = socket.split(); 315 + // Spawn a task that will push several messages to the client (does not matter what client does) 316 + let mut send_task = tokio::spawn(async move { 317 + const PING_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60); 318 + const BROADCAST_INTERVAL: std::time::Duration = std::time::Duration::from_secs(20); 319 + let mut last_ping = tokio::time::Instant::now(); 320 + let mut last_broadcast = tokio::time::Instant::now(); 321 + let mut n_msg = 0; 322 + loop { 323 + tokio::select! { 324 + _ = tokio::time::sleep_until(last_ping + PING_INTERVAL) => { 325 + tracing::debug!("Sending ping to {who}..."); 326 + if ws_send( 327 + &mut sender, 328 + who, 329 + Message::Ping(Bytes::from_static(&[1, 2, 3])) 330 + ).await.is_break() { 331 + tracing::warn!("Client {who} failed to respond to ping"); 332 + break; 333 + } 334 + tracing::debug!("Sent ping to {who}"); 335 + last_ping = tokio::time::Instant::now(); 336 + }, 337 + _ = tokio::time::sleep_until(last_broadcast + BROADCAST_INTERVAL) => { 338 + tracing::debug!("Polling for new messages to send to {who}..."); 339 + let current_cursor_count = get_current_cursor_count(&pool).await.unwrap_or_default(); 340 + if cursor < current_cursor_count { 341 + let missed_messages = get_missed_messages(&pool, cursor).await; 342 + if missed_messages.is_err() { 343 + tracing::warn!("Error getting missed messages: {missed_messages:?}"); 344 + last_broadcast = tokio::time::Instant::now(); 345 + continue; 346 + } 347 + for message in missed_messages.expect("Expected to be able to get missed messages but failed.") { 348 + let seq = message.seq; 349 + let neg = message.labels[0].neg; 350 + let uri = message.labels[0].uri.clone(); 351 + let val = message.labels[0].val.clone(); 352 + let prefix = if neg { "Negation" } else { "Emitting" }; 353 + tracing::info!("{prefix} label {seq} to {who}: {uri} {val}"); 354 + let message_header: Vec<u8> = Bytes::from_static(b"\xa2atg#labelsbop\x01").into(); 355 + let message_body = serde_cbor::to_vec(&message).expect("Expected to be able to serialize message to CBOR but failed."); 356 + let message_combined = [message_header, message_body].concat(); 357 + let message_finished = Message::Binary(message_combined.into()); 358 + if ws_send( 359 + &mut sender, 360 + who, 361 + message_finished 362 + ).await.is_break() { 363 + tracing::warn!("Client {who} failed to receive missed message"); 364 + break; 365 + } 366 + n_msg += 1; 367 + } 368 + cursor = current_cursor_count; 369 + } 370 + tracing::debug!("Finished poll for {who}"); 371 + last_broadcast = tokio::time::Instant::now(); 372 + } 373 + } 374 + } 375 + tracing::info!("Sending close to {who}..."); 376 + ws_close(sender).await; 377 + n_msg 378 + }); 379 + 380 + // This second task will receive messages from client and print them on server console 381 + let mut recv_task = tokio::spawn(async move { 382 + let mut cnt = 0; 383 + while let Some(Ok(msg)) = receiver.next().await { 384 + cnt += 1; 385 + // print message and break if instructed to do so 386 + if process_message(msg, who).is_break() { 387 + break; 388 + } 389 + } 390 + cnt 391 + }); 392 + 393 + // If any one of the tasks exit, abort the other. 394 + tokio::select! { 395 + rv_a = (&mut send_task) => { 396 + match rv_a { 397 + Ok(a) => tracing::info!("{a} messages sent to {who}"), 398 + Err(a) => tracing::warn!("Error sending messages {a:?}") 399 + } 400 + recv_task.abort(); 401 + }, 402 + rv_b = (&mut recv_task) => { 403 + match rv_b { 404 + Ok(b) => tracing::info!("Received {b} messages"), 405 + Err(b) => tracing::warn!("Error receiving messages {b:?}") 406 + } 407 + send_task.abort(); 408 + } 409 + } 410 + ControlFlow::Continue(()) 411 + } 412 + 413 + async fn ws_send( 414 + socket: &mut (impl SinkExt<Message> + Unpin), 415 + who: SocketAddr, 416 + msg: Message, 417 + ) -> ControlFlow<(), ()> { 418 + if socket 419 + .send(msg) 420 + .await 421 + .is_err() 422 + { 423 + tracing::warn!("client {who} abruptly disconnected"); 424 + return ControlFlow::Break(()); 425 + } 426 + ControlFlow::Continue(()) 427 + } 428 + 429 + async fn ws_close(mut sender: futures::stream::SplitSink<WebSocket, Message>) { 430 + if let Err(e) = sender 431 + .send(Message::Close(Some(CloseFrame { 432 + code: axum::extract::ws::close_code::NORMAL, 433 + reason: Utf8Bytes::from_static("Goodbye"), 434 + }))) 435 + .await 436 + { 437 + tracing::warn!("Could not send Close due to {e}, probably it is ok?"); 438 + } 439 + } 440 + 441 + /// helper to print contents of messages to stdout. Has special treatment for Close. 442 + #[allow(clippy::cognitive_complexity)] 443 + fn process_message(msg: Message, who: SocketAddr) -> ControlFlow<(), ()> { 444 + match msg { 445 + Message::Text(t) => { 446 + tracing::debug!(">>> {who} sent str: {t:?}"); 447 + } 448 + Message::Binary(d) => { 449 + tracing::debug!(">>> {} sent {} bytes: {:?}", who, d.len(), d); 450 + } 451 + Message::Close(c) => { 452 + if let Some(cf) = c { 453 + tracing::debug!( 454 + ">>> {} sent close with code {} and reason `{}`", 455 + who, cf.code, cf.reason 456 + ); 457 + } else { 458 + tracing::debug!(">>> {who} somehow sent close message without CloseFrame"); 459 + } 460 + return ControlFlow::Break(()); 461 + } 462 + 463 + Message::Pong(v) => { 464 + tracing::debug!(">>> {who} sent pong with {v:?}"); 465 + } 466 + // You should never need to manually handle Message::Ping, as axum's websocket library 467 + // will do so for you automagically by replying with Pong and copying the v according to 468 + // spec. But if you need the contents of the pings you can see them here. 469 + Message::Ping(v) => { 470 + tracing::debug!(">>> {who} sent ping with {v:?}"); 471 + } 472 + } 473 + ControlFlow::Continue(()) 474 + } 475 + 476 + /// WIP: fetch likes from app.bsky.feed.like 477 + /// https://shimeji.us-east.host.bsky.network/xrpc/com.atproto.repo.listRecords?repo=did:plc:jrtgsidnmxaen4offglr5lsh&collection=app.bsky.feed.like&limit=100 478 + /// 479 + /// then return them as a custom feed 480 + /// request path is at "/xrpc/app.bsky.feed.getFeedSkeleton?<feed>&<limit>&<cursor>" 481 + #[allow(dead_code)] 482 + async fn get_feed_skeleton(Query(params): Query<UriParams>) -> impl IntoResponse { 483 + if let Some(_uri_patterns) = &params.uriPatterns { 484 + let mut _agent = Agent::default(); 485 + 486 + } 487 + (StatusCode::OK, Json(serde_json::json!({}))) 488 + }