learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs

feat: add article import and saving

Changed files
+787 -315
crates
cli
server
web
+350 -270
Cargo.lock
··· 116 "p256", 117 "p384", 118 "rand 0.8.5", 119 - "reqwest 0.12.28", 120 "serde", 121 "serde_ipld_dagcbor", 122 "serde_json", ··· 136 "async-trait", 137 "atproto-identity", 138 "futures", 139 - "http 1.4.0", 140 "serde", 141 "serde_json", 142 "thiserror 2.0.17", ··· 165 "bytes", 166 "form_urlencoded", 167 "futures-util", 168 - "http 1.4.0", 169 - "http-body 1.0.1", 170 "http-body-util", 171 - "hyper 1.8.1", 172 "hyper-util", 173 "itoa", 174 "matchit", ··· 180 "serde_json", 181 "serde_path_to_error", 182 "serde_urlencoded", 183 - "sync_wrapper 1.0.2", 184 "tokio", 185 "tower", 186 "tower-layer", ··· 196 dependencies = [ 197 "bytes", 198 "futures-core", 199 - "http 1.4.0", 200 - "http-body 1.0.1", 201 "http-body-util", 202 "mime", 203 "pin-project-lite", 204 - "sync_wrapper 1.0.2", 205 "tower-layer", 206 "tower-service", 207 "tracing", ··· 231 232 [[package]] 233 name = "base64" 234 - version = "0.21.7" 235 - source = "registry+https://github.com/rust-lang/crates.io-index" 236 - checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 237 - 238 - [[package]] 239 - name = "base64" 240 version = "0.22.1" 241 source = "registry+https://github.com/rust-lang/crates.io-index" 242 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 246 version = "1.8.1" 247 source = "registry+https://github.com/rust-lang/crates.io-index" 248 checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" 249 - 250 - [[package]] 251 - name = "bitflags" 252 - version = "1.3.2" 253 - source = "registry+https://github.com/rust-lang/crates.io-index" 254 - checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 255 256 [[package]] 257 name = "bitflags" ··· 306 "libc", 307 "shlex", 308 ] 309 310 [[package]] 311 name = "cfg-if" ··· 394 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 395 396 [[package]] 397 name = "const-oid" 398 version = "0.9.6" 399 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 513 ] 514 515 [[package]] 516 name = "curve25519-dalek" 517 version = "4.1.3" 518 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 621 ] 622 623 [[package]] 624 name = "digest" 625 version = "0.10.7" 626 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 644 ] 645 646 [[package]] 647 name = "dotenvy" 648 version = "0.15.7" 649 source = "registry+https://github.com/rust-lang/crates.io-index" 650 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 651 652 [[package]] 653 name = "ecdsa" 654 version = "0.16.9" 655 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 784 checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" 785 786 [[package]] 787 name = "fnv" 788 version = "1.0.7" 789 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 919 ] 920 921 [[package]] 922 name = "generic-array" 923 version = "0.14.7" 924 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 957 ] 958 959 [[package]] 960 name = "group" 961 version = "0.13.0" 962 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 969 970 [[package]] 971 name = "h2" 972 - version = "0.3.27" 973 - source = "registry+https://github.com/rust-lang/crates.io-index" 974 - checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" 975 - dependencies = [ 976 - "bytes", 977 - "fnv", 978 - "futures-core", 979 - "futures-sink", 980 - "futures-util", 981 - "http 0.2.12", 982 - "indexmap", 983 - "slab", 984 - "tokio", 985 - "tokio-util", 986 - "tracing", 987 - ] 988 - 989 - [[package]] 990 - name = "h2" 991 version = "0.4.12" 992 source = "registry+https://github.com/rust-lang/crates.io-index" 993 checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" ··· 997 "fnv", 998 "futures-core", 999 "futures-sink", 1000 - "http 1.4.0", 1001 "indexmap", 1002 "slab", 1003 "tokio", ··· 1144 ] 1145 1146 [[package]] 1147 name = "html5ever" 1148 - version = "0.26.0" 1149 source = "registry+https://github.com/rust-lang/crates.io-index" 1150 - checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" 1151 dependencies = [ 1152 "log", 1153 "mac", 1154 - "markup5ever", 1155 "proc-macro2", 1156 "quote", 1157 - "syn 1.0.109", 1158 ] 1159 1160 [[package]] 1161 - name = "http" 1162 - version = "0.2.12" 1163 source = "registry+https://github.com/rust-lang/crates.io-index" 1164 - checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 1165 dependencies = [ 1166 - "bytes", 1167 - "fnv", 1168 - "itoa", 1169 ] 1170 1171 [[package]] ··· 1180 1181 [[package]] 1182 name = "http-body" 1183 - version = "0.4.6" 1184 - source = "registry+https://github.com/rust-lang/crates.io-index" 1185 - checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 1186 - dependencies = [ 1187 - "bytes", 1188 - "http 0.2.12", 1189 - "pin-project-lite", 1190 - ] 1191 - 1192 - [[package]] 1193 - name = "http-body" 1194 version = "1.0.1" 1195 source = "registry+https://github.com/rust-lang/crates.io-index" 1196 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1197 dependencies = [ 1198 "bytes", 1199 - "http 1.4.0", 1200 ] 1201 1202 [[package]] ··· 1207 dependencies = [ 1208 "bytes", 1209 "futures-core", 1210 - "http 1.4.0", 1211 - "http-body 1.0.1", 1212 "pin-project-lite", 1213 ] 1214 ··· 1226 1227 [[package]] 1228 name = "hyper" 1229 - version = "0.14.32" 1230 - source = "registry+https://github.com/rust-lang/crates.io-index" 1231 - checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 1232 - dependencies = [ 1233 - "bytes", 1234 - "futures-channel", 1235 - "futures-core", 1236 - "futures-util", 1237 - "h2 0.3.27", 1238 - "http 0.2.12", 1239 - "http-body 0.4.6", 1240 - "httparse", 1241 - "httpdate", 1242 - "itoa", 1243 - "pin-project-lite", 1244 - "socket2 0.5.10", 1245 - "tokio", 1246 - "tower-service", 1247 - "tracing", 1248 - "want", 1249 - ] 1250 - 1251 - [[package]] 1252 - name = "hyper" 1253 version = "1.8.1" 1254 source = "registry+https://github.com/rust-lang/crates.io-index" 1255 checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" ··· 1258 "bytes", 1259 "futures-channel", 1260 "futures-core", 1261 - "h2 0.4.12", 1262 - "http 1.4.0", 1263 - "http-body 1.0.1", 1264 "httparse", 1265 "httpdate", 1266 "itoa", ··· 1277 source = "registry+https://github.com/rust-lang/crates.io-index" 1278 checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1279 dependencies = [ 1280 - "http 1.4.0", 1281 - "hyper 1.8.1", 1282 "hyper-util", 1283 "rustls", 1284 "rustls-pki-types", ··· 1290 1291 [[package]] 1292 name = "hyper-tls" 1293 - version = "0.5.0" 1294 - source = "registry+https://github.com/rust-lang/crates.io-index" 1295 - checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 1296 - dependencies = [ 1297 - "bytes", 1298 - "hyper 0.14.32", 1299 - "native-tls", 1300 - "tokio", 1301 - "tokio-native-tls", 1302 - ] 1303 - 1304 - [[package]] 1305 - name = "hyper-tls" 1306 version = "0.6.0" 1307 source = "registry+https://github.com/rust-lang/crates.io-index" 1308 checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1309 dependencies = [ 1310 "bytes", 1311 "http-body-util", 1312 - "hyper 1.8.1", 1313 "hyper-util", 1314 "native-tls", 1315 "tokio", ··· 1323 source = "registry+https://github.com/rust-lang/crates.io-index" 1324 checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 1325 dependencies = [ 1326 - "base64 0.22.1", 1327 "bytes", 1328 "futures-channel", 1329 "futures-core", 1330 "futures-util", 1331 - "http 1.4.0", 1332 - "http-body 1.0.1", 1333 - "hyper 1.8.1", 1334 "ipnet", 1335 "libc", 1336 "percent-encoding", 1337 "pin-project-lite", 1338 "socket2 0.6.1", 1339 - "system-configuration 0.6.1", 1340 "tokio", 1341 "tower-service", 1342 "tracing", ··· 1529 version = "1.0.17" 1530 source = "registry+https://github.com/rust-lang/crates.io-index" 1531 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1532 1533 [[package]] 1534 name = "jobserver" ··· 1582 source = "registry+https://github.com/rust-lang/crates.io-index" 1583 checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 1584 dependencies = [ 1585 - "bitflags 2.10.0", 1586 "libc", 1587 "redox_syscall 0.7.0", 1588 ] ··· 1656 dependencies = [ 1657 "chrono", 1658 "clap", 1659 "dotenvy", 1660 "malfestio-core", 1661 "malfestio-server", 1662 "tokio", 1663 "tokio-postgres", 1664 ] ··· 1681 "async-trait", 1682 "atproto-jetstream", 1683 "axum", 1684 - "base64 0.22.1", 1685 "chrono", 1686 "deadpool-postgres", 1687 "dotenvy", 1688 "ed25519-dalek", 1689 "getrandom 0.3.4", 1690 "hickory-resolver 0.24.4", 1691 "malfestio-core", 1692 - "readability", 1693 "regex", 1694 - "reqwest 0.12.28", 1695 "serde", 1696 "serde_json", 1697 "serde_qs", ··· 1710 1711 [[package]] 1712 name = "markup5ever" 1713 - version = "0.11.0" 1714 source = "registry+https://github.com/rust-lang/crates.io-index" 1715 - checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" 1716 dependencies = [ 1717 "log", 1718 - "phf 0.10.1", 1719 "phf_codegen", 1720 "string_cache", 1721 "string_cache_codegen", ··· 1724 1725 [[package]] 1726 name = "markup5ever_rcdom" 1727 - version = "0.2.0" 1728 source = "registry+https://github.com/rust-lang/crates.io-index" 1729 - checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" 1730 dependencies = [ 1731 - "html5ever", 1732 - "markup5ever", 1733 "tendril", 1734 "xml5ever", 1735 ] ··· 1743 "proc-macro2", 1744 "quote", 1745 "syn 1.0.109", 1746 ] 1747 1748 [[package]] ··· 1912 source = "registry+https://github.com/rust-lang/crates.io-index" 1913 checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 1914 dependencies = [ 1915 - "bitflags 2.10.0", 1916 "cfg-if", 1917 "foreign-types", 1918 "libc", ··· 2014 2015 [[package]] 2016 name = "phf" 2017 - version = "0.10.1" 2018 source = "registry+https://github.com/rust-lang/crates.io-index" 2019 - checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" 2020 dependencies = [ 2021 - "phf_shared 0.10.0", 2022 ] 2023 2024 [[package]] ··· 2033 2034 [[package]] 2035 name = "phf_codegen" 2036 - version = "0.10.0" 2037 source = "registry+https://github.com/rust-lang/crates.io-index" 2038 - checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" 2039 dependencies = [ 2040 - "phf_generator 0.10.0", 2041 - "phf_shared 0.10.0", 2042 - ] 2043 - 2044 - [[package]] 2045 - name = "phf_generator" 2046 - version = "0.10.0" 2047 - source = "registry+https://github.com/rust-lang/crates.io-index" 2048 - checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 2049 - dependencies = [ 2050 - "phf_shared 0.10.0", 2051 - "rand 0.8.5", 2052 ] 2053 2054 [[package]] ··· 2062 ] 2063 2064 [[package]] 2065 - name = "phf_shared" 2066 - version = "0.10.0" 2067 source = "registry+https://github.com/rust-lang/crates.io-index" 2068 - checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 2069 dependencies = [ 2070 - "siphasher 0.3.11", 2071 ] 2072 2073 [[package]] ··· 2076 source = "registry+https://github.com/rust-lang/crates.io-index" 2077 checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2078 dependencies = [ 2079 - "siphasher 1.0.1", 2080 ] 2081 2082 [[package]] ··· 2085 source = "registry+https://github.com/rust-lang/crates.io-index" 2086 checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" 2087 dependencies = [ 2088 - "siphasher 1.0.1", 2089 ] 2090 2091 [[package]] ··· 2128 source = "registry+https://github.com/rust-lang/crates.io-index" 2129 checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" 2130 dependencies = [ 2131 - "base64 0.22.1", 2132 "byteorder", 2133 "bytes", 2134 "fallible-iterator", ··· 2333 ] 2334 2335 [[package]] 2336 - name = "readability" 2337 - version = "0.3.0" 2338 - source = "registry+https://github.com/rust-lang/crates.io-index" 2339 - checksum = "e56596e20a6d3cf715182d9b6829220621e6e985cec04d00410cee29821b4220" 2340 - dependencies = [ 2341 - "html5ever", 2342 - "lazy_static", 2343 - "markup5ever_rcdom", 2344 - "regex", 2345 - "reqwest 0.11.27", 2346 - "url", 2347 - ] 2348 - 2349 - [[package]] 2350 name = "redox_syscall" 2351 version = "0.5.18" 2352 source = "registry+https://github.com/rust-lang/crates.io-index" 2353 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2354 dependencies = [ 2355 - "bitflags 2.10.0", 2356 ] 2357 2358 [[package]] ··· 2361 source = "registry+https://github.com/rust-lang/crates.io-index" 2362 checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" 2363 dependencies = [ 2364 - "bitflags 2.10.0", 2365 ] 2366 2367 [[package]] ··· 2395 2396 [[package]] 2397 name = "reqwest" 2398 - version = "0.11.27" 2399 - source = "registry+https://github.com/rust-lang/crates.io-index" 2400 - checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 2401 - dependencies = [ 2402 - "base64 0.21.7", 2403 - "bytes", 2404 - "encoding_rs", 2405 - "futures-core", 2406 - "futures-util", 2407 - "h2 0.3.27", 2408 - "http 0.2.12", 2409 - "http-body 0.4.6", 2410 - "hyper 0.14.32", 2411 - "hyper-tls 0.5.0", 2412 - "ipnet", 2413 - "js-sys", 2414 - "log", 2415 - "mime", 2416 - "native-tls", 2417 - "once_cell", 2418 - "percent-encoding", 2419 - "pin-project-lite", 2420 - "rustls-pemfile", 2421 - "serde", 2422 - "serde_json", 2423 - "serde_urlencoded", 2424 - "sync_wrapper 0.1.2", 2425 - "system-configuration 0.5.1", 2426 - "tokio", 2427 - "tokio-native-tls", 2428 - "tower-service", 2429 - "url", 2430 - "wasm-bindgen", 2431 - "wasm-bindgen-futures", 2432 - "web-sys", 2433 - "winreg", 2434 - ] 2435 - 2436 - [[package]] 2437 - name = "reqwest" 2438 version = "0.12.28" 2439 source = "registry+https://github.com/rust-lang/crates.io-index" 2440 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 2441 dependencies = [ 2442 - "base64 0.22.1", 2443 "bytes", 2444 "encoding_rs", 2445 "futures-core", 2446 - "h2 0.4.12", 2447 - "http 1.4.0", 2448 - "http-body 1.0.1", 2449 "http-body-util", 2450 - "hyper 1.8.1", 2451 "hyper-rustls", 2452 - "hyper-tls 0.6.0", 2453 "hyper-util", 2454 "js-sys", 2455 "log", ··· 2463 "serde", 2464 "serde_json", 2465 "serde_urlencoded", 2466 - "sync_wrapper 1.0.2", 2467 "tokio", 2468 "tokio-native-tls", 2469 "tokio-rustls", ··· 2528 source = "registry+https://github.com/rust-lang/crates.io-index" 2529 checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 2530 dependencies = [ 2531 - "bitflags 2.10.0", 2532 "errno", 2533 "libc", 2534 "linux-raw-sys", ··· 2562 ] 2563 2564 [[package]] 2565 - name = "rustls-pemfile" 2566 - version = "1.0.4" 2567 - source = "registry+https://github.com/rust-lang/crates.io-index" 2568 - checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 2569 - dependencies = [ 2570 - "base64 0.21.7", 2571 - ] 2572 - 2573 - [[package]] 2574 name = "rustls-pki-types" 2575 version = "1.13.2" 2576 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2604 checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 2605 2606 [[package]] 2607 name = "schannel" 2608 version = "0.1.28" 2609 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2639 source = "registry+https://github.com/rust-lang/crates.io-index" 2640 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2641 dependencies = [ 2642 - "bitflags 2.10.0", 2643 "core-foundation 0.9.4", 2644 "core-foundation-sys", 2645 "libc", ··· 2652 source = "registry+https://github.com/rust-lang/crates.io-index" 2653 checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 2654 dependencies = [ 2655 - "bitflags 2.10.0", 2656 "core-foundation 0.10.1", 2657 "core-foundation-sys", 2658 "libc", ··· 2670 ] 2671 2672 [[package]] 2673 name = "semver" 2674 version = "1.0.27" 2675 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2785 ] 2786 2787 [[package]] 2788 name = "sha2" 2789 version = "0.10.9" 2790 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2838 2839 [[package]] 2840 name = "siphasher" 2841 - version = "0.3.11" 2842 - source = "registry+https://github.com/rust-lang/crates.io-index" 2843 - checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 2844 - 2845 - [[package]] 2846 - name = "siphasher" 2847 version = "1.0.1" 2848 source = "registry+https://github.com/rust-lang/crates.io-index" 2849 checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" ··· 2915 source = "registry+https://github.com/rust-lang/crates.io-index" 2916 checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 2917 dependencies = [ 2918 - "phf_generator 0.11.3", 2919 "phf_shared 0.11.3", 2920 "proc-macro2", 2921 "quote", ··· 2968 2969 [[package]] 2970 name = "sync_wrapper" 2971 - version = "0.1.2" 2972 - source = "registry+https://github.com/rust-lang/crates.io-index" 2973 - checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 2974 - 2975 - [[package]] 2976 - name = "sync_wrapper" 2977 version = "1.0.2" 2978 source = "registry+https://github.com/rust-lang/crates.io-index" 2979 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" ··· 2994 2995 [[package]] 2996 name = "system-configuration" 2997 - version = "0.5.1" 2998 - source = "registry+https://github.com/rust-lang/crates.io-index" 2999 - checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 3000 - dependencies = [ 3001 - "bitflags 1.3.2", 3002 - "core-foundation 0.9.4", 3003 - "system-configuration-sys 0.5.0", 3004 - ] 3005 - 3006 - [[package]] 3007 - name = "system-configuration" 3008 version = "0.6.1" 3009 source = "registry+https://github.com/rust-lang/crates.io-index" 3010 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3011 dependencies = [ 3012 - "bitflags 2.10.0", 3013 "core-foundation 0.9.4", 3014 - "system-configuration-sys 0.6.0", 3015 - ] 3016 - 3017 - [[package]] 3018 - name = "system-configuration-sys" 3019 - version = "0.5.0" 3020 - source = "registry+https://github.com/rust-lang/crates.io-index" 3021 - checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 3022 - dependencies = [ 3023 - "core-foundation-sys", 3024 - "libc", 3025 ] 3026 3027 [[package]] ··· 3263 source = "registry+https://github.com/rust-lang/crates.io-index" 3264 checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" 3265 dependencies = [ 3266 - "base64 0.22.1", 3267 "bytes", 3268 "futures-core", 3269 "futures-sink", 3270 - "http 1.4.0", 3271 "httparse", 3272 "rand 0.9.2", 3273 "ring", ··· 3288 "futures-core", 3289 "futures-util", 3290 "pin-project-lite", 3291 - "sync_wrapper 1.0.2", 3292 "tokio", 3293 "tower-layer", 3294 "tower-service", ··· 3304 "axum-core", 3305 "cookie", 3306 "futures-util", 3307 - "http 1.4.0", 3308 "parking_lot", 3309 "pin-project-lite", 3310 "tower-layer", ··· 3317 source = "registry+https://github.com/rust-lang/crates.io-index" 3318 checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 3319 dependencies = [ 3320 - "bitflags 2.10.0", 3321 "bytes", 3322 "futures-util", 3323 - "http 1.4.0", 3324 - "http-body 1.0.1", 3325 "iri-string", 3326 "pin-project-lite", 3327 "tower", ··· 3444 checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 3445 3446 [[package]] 3447 name = "unsigned-varint" 3448 version = "0.8.0" 3449 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3480 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 3481 3482 [[package]] 3483 name = "utf8_iter" 3484 version = "1.0.4" 3485 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3522 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3523 3524 [[package]] 3525 name = "want" 3526 version = "0.3.1" 3527 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3654 version = "1.2.1" 3655 source = "registry+https://github.com/rust-lang/crates.io-index" 3656 checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 3657 3658 [[package]] 3659 name = "windows-core" ··· 3971 3972 [[package]] 3973 name = "xml5ever" 3974 - version = "0.17.0" 3975 source = "registry+https://github.com/rust-lang/crates.io-index" 3976 - checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" 3977 dependencies = [ 3978 "log", 3979 "mac", 3980 - "markup5ever", 3981 ] 3982 3983 [[package]]
··· 116 "p256", 117 "p384", 118 "rand 0.8.5", 119 + "reqwest", 120 "serde", 121 "serde_ipld_dagcbor", 122 "serde_json", ··· 136 "async-trait", 137 "atproto-identity", 138 "futures", 139 + "http", 140 "serde", 141 "serde_json", 142 "thiserror 2.0.17", ··· 165 "bytes", 166 "form_urlencoded", 167 "futures-util", 168 + "http", 169 + "http-body", 170 "http-body-util", 171 + "hyper", 172 "hyper-util", 173 "itoa", 174 "matchit", ··· 180 "serde_json", 181 "serde_path_to_error", 182 "serde_urlencoded", 183 + "sync_wrapper", 184 "tokio", 185 "tower", 186 "tower-layer", ··· 196 dependencies = [ 197 "bytes", 198 "futures-core", 199 + "http", 200 + "http-body", 201 "http-body-util", 202 "mime", 203 "pin-project-lite", 204 + "sync_wrapper", 205 "tower-layer", 206 "tower-service", 207 "tracing", ··· 231 232 [[package]] 233 name = "base64" 234 version = "0.22.1" 235 source = "registry+https://github.com/rust-lang/crates.io-index" 236 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 240 version = "1.8.1" 241 source = "registry+https://github.com/rust-lang/crates.io-index" 242 checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" 243 244 [[package]] 245 name = "bitflags" ··· 294 "libc", 295 "shlex", 296 ] 297 + 298 + [[package]] 299 + name = "cesu8" 300 + version = "1.1.0" 301 + source = "registry+https://github.com/rust-lang/crates.io-index" 302 + checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 303 304 [[package]] 305 name = "cfg-if" ··· 388 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 389 390 [[package]] 391 + name = "combine" 392 + version = "4.6.7" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 395 + dependencies = [ 396 + "bytes", 397 + "memchr", 398 + ] 399 + 400 + [[package]] 401 name = "const-oid" 402 version = "0.9.6" 403 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 517 ] 518 519 [[package]] 520 + name = "cssparser" 521 + version = "0.34.0" 522 + source = "registry+https://github.com/rust-lang/crates.io-index" 523 + checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" 524 + dependencies = [ 525 + "cssparser-macros", 526 + "dtoa-short", 527 + "itoa", 528 + "phf 0.11.3", 529 + "smallvec", 530 + ] 531 + 532 + [[package]] 533 + name = "cssparser-macros" 534 + version = "0.6.1" 535 + source = "registry+https://github.com/rust-lang/crates.io-index" 536 + checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" 537 + dependencies = [ 538 + "quote", 539 + "syn 2.0.111", 540 + ] 541 + 542 + [[package]] 543 name = "curve25519-dalek" 544 version = "4.1.3" 545 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 648 ] 649 650 [[package]] 651 + name = "derive_more" 652 + version = "0.99.20" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" 655 + dependencies = [ 656 + "proc-macro2", 657 + "quote", 658 + "syn 2.0.111", 659 + ] 660 + 661 + [[package]] 662 name = "digest" 663 version = "0.10.7" 664 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 682 ] 683 684 [[package]] 685 + name = "dom_query" 686 + version = "0.12.0" 687 + source = "registry+https://github.com/rust-lang/crates.io-index" 688 + checksum = "688b93023aba6768721b48ec5588308e45ac42d788c6dd974d1c2b9a1d04ea29" 689 + dependencies = [ 690 + "cssparser", 691 + "foldhash", 692 + "html5ever 0.29.1", 693 + "precomputed-hash", 694 + "selectors", 695 + "tendril", 696 + ] 697 + 698 + [[package]] 699 + name = "dom_smoothie" 700 + version = "0.4.0" 701 + source = "registry+https://github.com/rust-lang/crates.io-index" 702 + checksum = "d23bf500fc0a79f9bf12c38816574820929ecf4f6b39ec07743f7ed485439c31" 703 + dependencies = [ 704 + "dom_query", 705 + "flagset", 706 + "gjson", 707 + "html-escape", 708 + "once_cell", 709 + "phf 0.11.3", 710 + "regex", 711 + "tendril", 712 + "thiserror 2.0.17", 713 + "unicode-segmentation", 714 + "url", 715 + ] 716 + 717 + [[package]] 718 name = "dotenvy" 719 version = "0.15.7" 720 source = "registry+https://github.com/rust-lang/crates.io-index" 721 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 722 723 [[package]] 724 + name = "dtoa" 725 + version = "1.0.11" 726 + source = "registry+https://github.com/rust-lang/crates.io-index" 727 + checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" 728 + 729 + [[package]] 730 + name = "dtoa-short" 731 + version = "0.3.5" 732 + source = "registry+https://github.com/rust-lang/crates.io-index" 733 + checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" 734 + dependencies = [ 735 + "dtoa", 736 + ] 737 + 738 + [[package]] 739 name = "ecdsa" 740 version = "0.16.9" 741 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 870 checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" 871 872 [[package]] 873 + name = "flagset" 874 + version = "0.4.7" 875 + source = "registry+https://github.com/rust-lang/crates.io-index" 876 + checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" 877 + 878 + [[package]] 879 name = "fnv" 880 version = "1.0.7" 881 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1011 ] 1012 1013 [[package]] 1014 + name = "fxhash" 1015 + version = "0.2.1" 1016 + source = "registry+https://github.com/rust-lang/crates.io-index" 1017 + checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 1018 + dependencies = [ 1019 + "byteorder", 1020 + ] 1021 + 1022 + [[package]] 1023 name = "generic-array" 1024 version = "0.14.7" 1025 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1058 ] 1059 1060 [[package]] 1061 + name = "gjson" 1062 + version = "0.8.1" 1063 + source = "registry+https://github.com/rust-lang/crates.io-index" 1064 + checksum = "43503cc176394dd30a6525f5f36e838339b8b5619be33ed9a7783841580a97b6" 1065 + 1066 + [[package]] 1067 name = "group" 1068 version = "0.13.0" 1069 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1076 1077 [[package]] 1078 name = "h2" 1079 version = "0.4.12" 1080 source = "registry+https://github.com/rust-lang/crates.io-index" 1081 checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" ··· 1085 "fnv", 1086 "futures-core", 1087 "futures-sink", 1088 + "http", 1089 "indexmap", 1090 "slab", 1091 "tokio", ··· 1232 ] 1233 1234 [[package]] 1235 + name = "html-escape" 1236 + version = "0.2.13" 1237 + source = "registry+https://github.com/rust-lang/crates.io-index" 1238 + checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" 1239 + dependencies = [ 1240 + "utf8-width", 1241 + ] 1242 + 1243 + [[package]] 1244 + name = "html2md" 1245 + version = "0.2.15" 1246 + source = "registry+https://github.com/rust-lang/crates.io-index" 1247 + checksum = "8cff9891f2e0d9048927fbdfc28b11bf378f6a93c7ba70b23d0fbee9af6071b4" 1248 + dependencies = [ 1249 + "html5ever 0.27.0", 1250 + "jni", 1251 + "lazy_static", 1252 + "markup5ever_rcdom", 1253 + "percent-encoding", 1254 + "regex", 1255 + ] 1256 + 1257 + [[package]] 1258 name = "html5ever" 1259 + version = "0.27.0" 1260 source = "registry+https://github.com/rust-lang/crates.io-index" 1261 + checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" 1262 dependencies = [ 1263 "log", 1264 "mac", 1265 + "markup5ever 0.12.1", 1266 "proc-macro2", 1267 "quote", 1268 + "syn 2.0.111", 1269 ] 1270 1271 [[package]] 1272 + name = "html5ever" 1273 + version = "0.29.1" 1274 source = "registry+https://github.com/rust-lang/crates.io-index" 1275 + checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" 1276 dependencies = [ 1277 + "log", 1278 + "mac", 1279 + "markup5ever 0.14.1", 1280 + "match_token", 1281 ] 1282 1283 [[package]] ··· 1292 1293 [[package]] 1294 name = "http-body" 1295 version = "1.0.1" 1296 source = "registry+https://github.com/rust-lang/crates.io-index" 1297 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1298 dependencies = [ 1299 "bytes", 1300 + "http", 1301 ] 1302 1303 [[package]] ··· 1308 dependencies = [ 1309 "bytes", 1310 "futures-core", 1311 + "http", 1312 + "http-body", 1313 "pin-project-lite", 1314 ] 1315 ··· 1327 1328 [[package]] 1329 name = "hyper" 1330 version = "1.8.1" 1331 source = "registry+https://github.com/rust-lang/crates.io-index" 1332 checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" ··· 1335 "bytes", 1336 "futures-channel", 1337 "futures-core", 1338 + "h2", 1339 + "http", 1340 + "http-body", 1341 "httparse", 1342 "httpdate", 1343 "itoa", ··· 1354 source = "registry+https://github.com/rust-lang/crates.io-index" 1355 checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1356 dependencies = [ 1357 + "http", 1358 + "hyper", 1359 "hyper-util", 1360 "rustls", 1361 "rustls-pki-types", ··· 1367 1368 [[package]] 1369 name = "hyper-tls" 1370 version = "0.6.0" 1371 source = "registry+https://github.com/rust-lang/crates.io-index" 1372 checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1373 dependencies = [ 1374 "bytes", 1375 "http-body-util", 1376 + "hyper", 1377 "hyper-util", 1378 "native-tls", 1379 "tokio", ··· 1387 source = "registry+https://github.com/rust-lang/crates.io-index" 1388 checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 1389 dependencies = [ 1390 + "base64", 1391 "bytes", 1392 "futures-channel", 1393 "futures-core", 1394 "futures-util", 1395 + "http", 1396 + "http-body", 1397 + "hyper", 1398 "ipnet", 1399 "libc", 1400 "percent-encoding", 1401 "pin-project-lite", 1402 "socket2 0.6.1", 1403 + "system-configuration", 1404 "tokio", 1405 "tower-service", 1406 "tracing", ··· 1593 version = "1.0.17" 1594 source = "registry+https://github.com/rust-lang/crates.io-index" 1595 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1596 + 1597 + [[package]] 1598 + name = "jni" 1599 + version = "0.19.0" 1600 + source = "registry+https://github.com/rust-lang/crates.io-index" 1601 + checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" 1602 + dependencies = [ 1603 + "cesu8", 1604 + "combine", 1605 + "jni-sys", 1606 + "log", 1607 + "thiserror 1.0.69", 1608 + "walkdir", 1609 + ] 1610 + 1611 + [[package]] 1612 + name = "jni-sys" 1613 + version = "0.3.0" 1614 + source = "registry+https://github.com/rust-lang/crates.io-index" 1615 + checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 1616 1617 [[package]] 1618 name = "jobserver" ··· 1666 source = "registry+https://github.com/rust-lang/crates.io-index" 1667 checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 1668 dependencies = [ 1669 + "bitflags", 1670 "libc", 1671 "redox_syscall 0.7.0", 1672 ] ··· 1740 dependencies = [ 1741 "chrono", 1742 "clap", 1743 + "dom_smoothie", 1744 "dotenvy", 1745 + "html2md", 1746 "malfestio-core", 1747 "malfestio-server", 1748 + "reqwest", 1749 "tokio", 1750 "tokio-postgres", 1751 ] ··· 1768 "async-trait", 1769 "atproto-jetstream", 1770 "axum", 1771 + "base64", 1772 "chrono", 1773 "deadpool-postgres", 1774 + "dom_smoothie", 1775 "dotenvy", 1776 "ed25519-dalek", 1777 "getrandom 0.3.4", 1778 "hickory-resolver 0.24.4", 1779 + "html2md", 1780 "malfestio-core", 1781 "regex", 1782 + "reqwest", 1783 "serde", 1784 "serde_json", 1785 "serde_qs", ··· 1798 1799 [[package]] 1800 name = "markup5ever" 1801 + version = "0.12.1" 1802 + source = "registry+https://github.com/rust-lang/crates.io-index" 1803 + checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" 1804 + dependencies = [ 1805 + "log", 1806 + "phf 0.11.3", 1807 + "phf_codegen", 1808 + "string_cache", 1809 + "string_cache_codegen", 1810 + "tendril", 1811 + ] 1812 + 1813 + [[package]] 1814 + name = "markup5ever" 1815 + version = "0.14.1" 1816 source = "registry+https://github.com/rust-lang/crates.io-index" 1817 + checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" 1818 dependencies = [ 1819 "log", 1820 + "phf 0.11.3", 1821 "phf_codegen", 1822 "string_cache", 1823 "string_cache_codegen", ··· 1826 1827 [[package]] 1828 name = "markup5ever_rcdom" 1829 + version = "0.3.0" 1830 source = "registry+https://github.com/rust-lang/crates.io-index" 1831 + checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" 1832 dependencies = [ 1833 + "html5ever 0.27.0", 1834 + "markup5ever 0.12.1", 1835 "tendril", 1836 "xml5ever", 1837 ] ··· 1845 "proc-macro2", 1846 "quote", 1847 "syn 1.0.109", 1848 + ] 1849 + 1850 + [[package]] 1851 + name = "match_token" 1852 + version = "0.1.0" 1853 + source = "registry+https://github.com/rust-lang/crates.io-index" 1854 + checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" 1855 + dependencies = [ 1856 + "proc-macro2", 1857 + "quote", 1858 + "syn 2.0.111", 1859 ] 1860 1861 [[package]] ··· 2025 source = "registry+https://github.com/rust-lang/crates.io-index" 2026 checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 2027 dependencies = [ 2028 + "bitflags", 2029 "cfg-if", 2030 "foreign-types", 2031 "libc", ··· 2127 2128 [[package]] 2129 name = "phf" 2130 + version = "0.11.3" 2131 source = "registry+https://github.com/rust-lang/crates.io-index" 2132 + checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 2133 dependencies = [ 2134 + "phf_macros", 2135 + "phf_shared 0.11.3", 2136 ] 2137 2138 [[package]] ··· 2147 2148 [[package]] 2149 name = "phf_codegen" 2150 + version = "0.11.3" 2151 source = "registry+https://github.com/rust-lang/crates.io-index" 2152 + checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 2153 dependencies = [ 2154 + "phf_generator", 2155 + "phf_shared 0.11.3", 2156 ] 2157 2158 [[package]] ··· 2166 ] 2167 2168 [[package]] 2169 + name = "phf_macros" 2170 + version = "0.11.3" 2171 source = "registry+https://github.com/rust-lang/crates.io-index" 2172 + checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 2173 dependencies = [ 2174 + "phf_generator", 2175 + "phf_shared 0.11.3", 2176 + "proc-macro2", 2177 + "quote", 2178 + "syn 2.0.111", 2179 ] 2180 2181 [[package]] ··· 2184 source = "registry+https://github.com/rust-lang/crates.io-index" 2185 checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2186 dependencies = [ 2187 + "siphasher", 2188 ] 2189 2190 [[package]] ··· 2193 source = "registry+https://github.com/rust-lang/crates.io-index" 2194 checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" 2195 dependencies = [ 2196 + "siphasher", 2197 ] 2198 2199 [[package]] ··· 2236 source = "registry+https://github.com/rust-lang/crates.io-index" 2237 checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" 2238 dependencies = [ 2239 + "base64", 2240 "byteorder", 2241 "bytes", 2242 "fallible-iterator", ··· 2441 ] 2442 2443 [[package]] 2444 name = "redox_syscall" 2445 version = "0.5.18" 2446 source = "registry+https://github.com/rust-lang/crates.io-index" 2447 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2448 dependencies = [ 2449 + "bitflags", 2450 ] 2451 2452 [[package]] ··· 2455 source = "registry+https://github.com/rust-lang/crates.io-index" 2456 checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" 2457 dependencies = [ 2458 + "bitflags", 2459 ] 2460 2461 [[package]] ··· 2489 2490 [[package]] 2491 name = "reqwest" 2492 version = "0.12.28" 2493 source = "registry+https://github.com/rust-lang/crates.io-index" 2494 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 2495 dependencies = [ 2496 + "base64", 2497 "bytes", 2498 "encoding_rs", 2499 "futures-core", 2500 + "h2", 2501 + "http", 2502 + "http-body", 2503 "http-body-util", 2504 + "hyper", 2505 "hyper-rustls", 2506 + "hyper-tls", 2507 "hyper-util", 2508 "js-sys", 2509 "log", ··· 2517 "serde", 2518 "serde_json", 2519 "serde_urlencoded", 2520 + "sync_wrapper", 2521 "tokio", 2522 "tokio-native-tls", 2523 "tokio-rustls", ··· 2582 source = "registry+https://github.com/rust-lang/crates.io-index" 2583 checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 2584 dependencies = [ 2585 + "bitflags", 2586 "errno", 2587 "libc", 2588 "linux-raw-sys", ··· 2616 ] 2617 2618 [[package]] 2619 name = "rustls-pki-types" 2620 version = "1.13.2" 2621 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2649 checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 2650 2651 [[package]] 2652 + name = "same-file" 2653 + version = "1.0.6" 2654 + source = "registry+https://github.com/rust-lang/crates.io-index" 2655 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 2656 + dependencies = [ 2657 + "winapi-util", 2658 + ] 2659 + 2660 + [[package]] 2661 name = "schannel" 2662 version = "0.1.28" 2663 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2693 source = "registry+https://github.com/rust-lang/crates.io-index" 2694 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2695 dependencies = [ 2696 + "bitflags", 2697 "core-foundation 0.9.4", 2698 "core-foundation-sys", 2699 "libc", ··· 2706 source = "registry+https://github.com/rust-lang/crates.io-index" 2707 checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 2708 dependencies = [ 2709 + "bitflags", 2710 "core-foundation 0.10.1", 2711 "core-foundation-sys", 2712 "libc", ··· 2724 ] 2725 2726 [[package]] 2727 + name = "selectors" 2728 + version = "0.26.0" 2729 + source = "registry+https://github.com/rust-lang/crates.io-index" 2730 + checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" 2731 + dependencies = [ 2732 + "bitflags", 2733 + "cssparser", 2734 + "derive_more", 2735 + "fxhash", 2736 + "log", 2737 + "new_debug_unreachable", 2738 + "phf 0.11.3", 2739 + "phf_codegen", 2740 + "precomputed-hash", 2741 + "servo_arc", 2742 + "smallvec", 2743 + ] 2744 + 2745 + [[package]] 2746 name = "semver" 2747 version = "1.0.27" 2748 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2858 ] 2859 2860 [[package]] 2861 + name = "servo_arc" 2862 + version = "0.4.3" 2863 + source = "registry+https://github.com/rust-lang/crates.io-index" 2864 + checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" 2865 + dependencies = [ 2866 + "stable_deref_trait", 2867 + ] 2868 + 2869 + [[package]] 2870 name = "sha2" 2871 version = "0.10.9" 2872 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2920 2921 [[package]] 2922 name = "siphasher" 2923 version = "1.0.1" 2924 source = "registry+https://github.com/rust-lang/crates.io-index" 2925 checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" ··· 2991 source = "registry+https://github.com/rust-lang/crates.io-index" 2992 checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 2993 dependencies = [ 2994 + "phf_generator", 2995 "phf_shared 0.11.3", 2996 "proc-macro2", 2997 "quote", ··· 3044 3045 [[package]] 3046 name = "sync_wrapper" 3047 version = "1.0.2" 3048 source = "registry+https://github.com/rust-lang/crates.io-index" 3049 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" ··· 3064 3065 [[package]] 3066 name = "system-configuration" 3067 version = "0.6.1" 3068 source = "registry+https://github.com/rust-lang/crates.io-index" 3069 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3070 dependencies = [ 3071 + "bitflags", 3072 "core-foundation 0.9.4", 3073 + "system-configuration-sys", 3074 ] 3075 3076 [[package]] ··· 3312 source = "registry+https://github.com/rust-lang/crates.io-index" 3313 checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" 3314 dependencies = [ 3315 + "base64", 3316 "bytes", 3317 "futures-core", 3318 "futures-sink", 3319 + "http", 3320 "httparse", 3321 "rand 0.9.2", 3322 "ring", ··· 3337 "futures-core", 3338 "futures-util", 3339 "pin-project-lite", 3340 + "sync_wrapper", 3341 "tokio", 3342 "tower-layer", 3343 "tower-service", ··· 3353 "axum-core", 3354 "cookie", 3355 "futures-util", 3356 + "http", 3357 "parking_lot", 3358 "pin-project-lite", 3359 "tower-layer", ··· 3366 source = "registry+https://github.com/rust-lang/crates.io-index" 3367 checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 3368 dependencies = [ 3369 + "bitflags", 3370 "bytes", 3371 "futures-util", 3372 + "http", 3373 + "http-body", 3374 "iri-string", 3375 "pin-project-lite", 3376 "tower", ··· 3493 checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 3494 3495 [[package]] 3496 + name = "unicode-segmentation" 3497 + version = "1.12.0" 3498 + source = "registry+https://github.com/rust-lang/crates.io-index" 3499 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 3500 + 3501 + [[package]] 3502 name = "unsigned-varint" 3503 version = "0.8.0" 3504 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3535 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 3536 3537 [[package]] 3538 + name = "utf8-width" 3539 + version = "0.1.8" 3540 + source = "registry+https://github.com/rust-lang/crates.io-index" 3541 + checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" 3542 + 3543 + [[package]] 3544 name = "utf8_iter" 3545 version = "1.0.4" 3546 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3583 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3584 3585 [[package]] 3586 + name = "walkdir" 3587 + version = "2.5.0" 3588 + source = "registry+https://github.com/rust-lang/crates.io-index" 3589 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 3590 + dependencies = [ 3591 + "same-file", 3592 + "winapi-util", 3593 + ] 3594 + 3595 + [[package]] 3596 name = "want" 3597 version = "0.3.1" 3598 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3725 version = "1.2.1" 3726 source = "registry+https://github.com/rust-lang/crates.io-index" 3727 checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 3728 + 3729 + [[package]] 3730 + name = "winapi-util" 3731 + version = "0.1.11" 3732 + source = "registry+https://github.com/rust-lang/crates.io-index" 3733 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 3734 + dependencies = [ 3735 + "windows-sys 0.61.2", 3736 + ] 3737 3738 [[package]] 3739 name = "windows-core" ··· 4051 4052 [[package]] 4053 name = "xml5ever" 4054 + version = "0.18.1" 4055 source = "registry+https://github.com/rust-lang/crates.io-index" 4056 + checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" 4057 dependencies = [ 4058 "log", 4059 "mac", 4060 + "markup5ever 0.12.1", 4061 ] 4062 4063 [[package]]
+3
crates/cli/Cargo.toml
··· 6 [dependencies] 7 chrono = "0.4" 8 clap = { version = "4.5.53", features = ["derive"] } 9 dotenvy = "0.15.7" 10 malfestio-core = { version = "0.1.0", path = "../core" } 11 malfestio-server = { version = "0.1.0", path = "../server" } 12 tokio = { version = "1.48.0", features = ["full"] } 13 tokio-postgres = "0.7.13"
··· 6 [dependencies] 7 chrono = "0.4" 8 clap = { version = "4.5.53", features = ["derive"] } 9 + dom_smoothie = "0.4" 10 dotenvy = "0.15.7" 11 + html2md = "0.2.15" 12 malfestio-core = { version = "0.1.0", path = "../core" } 13 malfestio-server = { version = "0.1.0", path = "../server" } 14 + reqwest = { version = "0.12", features = ["json"] } 15 tokio = { version = "1.48.0", features = ["full"] } 16 tokio-postgres = "0.7.13"
+123
crates/cli/src/main.rs
··· 28 /// Bluesky handle to test (e.g., alice.bsky.social) 29 handle: String, 30 }, 31 } 32 33 #[tokio::main] ··· 47 Commands::Check { handle } => { 48 check_flow(handle).await?; 49 } 50 } 51 52 Ok(()) ··· 289 format!("{} months ago", duration.num_days() / 30) 290 } 291 }
··· 28 /// Bluesky handle to test (e.g., alice.bsky.social) 29 handle: String, 30 }, 31 + #[cfg(debug_assertions)] 32 + /// [DEBUG ONLY] Debug utilities 33 + Debug { 34 + #[command(subcommand)] 35 + command: DebugCommands, 36 + }, 37 + } 38 + 39 + #[cfg(debug_assertions)] 40 + #[derive(Subcommand)] 41 + enum DebugCommands { 42 + /// Test article extraction and markdown conversion 43 + Article { 44 + /// Article URL to extract 45 + url: String, 46 + /// Save to file instead of printing to terminal 47 + #[arg(short, long)] 48 + output: Option<String>, 49 + }, 50 } 51 52 #[tokio::main] ··· 66 Commands::Check { handle } => { 67 check_flow(handle).await?; 68 } 69 + #[cfg(debug_assertions)] 70 + Commands::Debug { command } => match command { 71 + DebugCommands::Article { url, output } => { 72 + debug_article(url, output.as_deref()).await?; 73 + } 74 + }, 75 } 76 77 Ok(()) ··· 314 format!("{} months ago", duration.num_days() / 30) 315 } 316 } 317 + 318 + #[cfg(debug_assertions)] 319 + async fn debug_article(url: &str, output_file: Option<&str>) -> malfestio_core::Result<()> { 320 + use dom_smoothie::Readability; 321 + 322 + println!("Fetching article from: {}", url); 323 + 324 + // Fetch HTML content with user-agent 325 + let client = reqwest::Client::builder() 326 + .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") 327 + .build() 328 + .map_err(|e| malfestio_core::Error::Other(format!("Failed to build client: {}", e)))?; 329 + 330 + let response = client.get(url) 331 + .send() 332 + .await 333 + .map_err(|e| malfestio_core::Error::Other(format!("Failed to fetch URL: {}", e)))?; 334 + 335 + let html_content = response 336 + .text() 337 + .await 338 + .map_err(|e| malfestio_core::Error::Other(format!("Failed to read response: {}", e)))?; 339 + 340 + println!("Fetched {} bytes of HTML", html_content.len()); 341 + 342 + // Extract article using dom_smoothie 343 + println!("Extracting article content..."); 344 + let url_clone = url.to_string(); 345 + let result = tokio::task::spawn_blocking( 346 + move || -> Result<(String, String, Option<String>, Option<String>), String> { 347 + let mut readability = Readability::new(html_content, Some(&url_clone), None) 348 + .map_err(|e| format!("Readability error: {}", e))?; 349 + let article = readability.parse().map_err(|e| format!("Parse error: {}", e))?; 350 + Ok(( 351 + article.title, 352 + article.content.to_string(), 353 + article.byline, 354 + article.published_time, 355 + )) 356 + }, 357 + ) 358 + .await 359 + .map_err(|e| malfestio_core::Error::Other(format!("Task join error: {}", e)))? 360 + .map_err(malfestio_core::Error::Other)?; 361 + 362 + let (title, content, author, publish_date) = result; 363 + 364 + println!("✓ Extracted article:"); 365 + println!(" Title: {}", title); 366 + if let Some(author) = &author { 367 + println!(" Author: {}", author); 368 + } 369 + if let Some(date) = &publish_date { 370 + println!(" Published: {}", date); 371 + } 372 + println!(" Content length: {} bytes", content.len()); 373 + 374 + // Convert HTML to markdown 375 + println!("\nConverting to markdown..."); 376 + let markdown = html2md::parse_html(&content); 377 + println!("✓ Converted to {} bytes of markdown", markdown.len()); 378 + 379 + // Output 380 + if let Some(file_path) = output_file { 381 + println!("\nSaving to file: {}", file_path); 382 + 383 + let mut output = String::new(); 384 + output.push_str(&format!("# {}\n\n", title)); 385 + if let Some(author) = author { 386 + output.push_str(&format!("**Author:** {}\n", author)); 387 + } 388 + if let Some(date) = publish_date { 389 + output.push_str(&format!("**Published:** {}\n", date)); 390 + } 391 + output.push_str(&format!("**Source:** {}\n\n", url)); 392 + output.push_str("---\n\n"); 393 + output.push_str(&markdown); 394 + 395 + fs::write(file_path, output) 396 + .map_err(|e| malfestio_core::Error::Other(format!("Failed to write file: {}", e)))?; 397 + 398 + println!("✓ Saved to {}", file_path); 399 + } else { 400 + println!("\n{}", "=".repeat(80)); 401 + println!("# {}", title); 402 + if let Some(author) = author { 403 + println!("\n**Author:** {}", author); 404 + } 405 + if let Some(date) = publish_date { 406 + println!("**Published:** {}", date); 407 + } 408 + println!("**Source:** {}", url); 409 + println!("{}", "=".repeat(80)); 410 + println!("\n{}", markdown); 411 + } 412 + 413 + Ok(()) 414 + }
+4 -3
crates/server/Cargo.toml
··· 13 deadpool-postgres = "0.14.0" 14 dotenvy = "0.15.7" 15 ed25519-dalek = { version = "2.2.0", features = ["serde"] } 16 getrandom = { version = "0.3", features = ["std"] } 17 malfestio-core = { version = "0.1.0", path = "../core" } 18 - readability = "0.3.0" 19 regex = "1.12.2" 20 - reqwest = { version = "0.12.28", features = ["json"] } 21 serde = "1.0.228" 22 serde_json = "1.0.148" 23 serde_qs = "0.15" ··· 36 tracing = "0.1.44" 37 tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 38 uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } 39 - hickory-resolver = "0.24.0"
··· 13 deadpool-postgres = "0.14.0" 14 dotenvy = "0.15.7" 15 ed25519-dalek = { version = "2.2.0", features = ["serde"] } 16 + dom_smoothie = "0.4" 17 getrandom = { version = "0.3", features = ["std"] } 18 + html2md = "0.2.15" 19 malfestio-core = { version = "0.1.0", path = "../core" } 20 regex = "1.12.2" 21 + reqwest = { version = "0.12", features = ["json"] } 22 serde = "1.0.228" 23 serde_json = "1.0.148" 24 serde_qs = "0.15" ··· 37 tracing = "0.1.44" 38 tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 39 uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } 40 + hickory-resolver = "0.24"
+186 -13
crates/server/src/api/importer.rs
··· 1 - use axum::{Json, http::StatusCode, response::IntoResponse}; 2 - use readability::extractor; 3 - use serde::Deserialize; 4 use serde_json::json; 5 6 #[derive(Deserialize)] 7 pub struct ImportRequest { 8 url: String, 9 } 10 11 pub async fn import_article(Json(payload): Json<ImportRequest>) -> impl IntoResponse { ··· 14 } 15 16 let url = payload.url.clone(); 17 let url_for_task = url.clone(); 18 19 - let result = tokio::task::spawn_blocking(move || extractor::scrape(&url_for_task)).await; 20 21 match result { 22 - Ok(Ok(product)) => Json(json!({ 23 - "title": product.title, 24 - "content": product.content, 25 - "text": product.text, 26 - "url": url 27 - })) 28 - .into_response(), 29 Ok(Err(e)) => ( 30 StatusCode::INTERNAL_SERVER_ERROR, 31 - Json(json!({"error": format!("Readability error: {}", e)})), 32 ) 33 .into_response(), 34 Err(e) => ( ··· 60 let body_json: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); 61 let title = body_json["title"].as_str().unwrap(); 62 assert!(title.contains("Rust")); 63 - assert!(body_json["text"].as_str().unwrap().len() > 100); 64 } 65 66 #[tokio::test]
··· 1 + use crate::middleware::auth::UserContext; 2 + use crate::state::SharedState; 3 + use axum::{Json, extract::Extension, http::StatusCode, response::IntoResponse}; 4 + use dom_smoothie::Readability; 5 + use malfestio_core::model::Visibility; 6 + use serde::{Deserialize, Serialize}; 7 use serde_json::json; 8 9 #[derive(Deserialize)] 10 pub struct ImportRequest { 11 url: String, 12 + } 13 + 14 + #[derive(Serialize)] 15 + pub struct ArticleMetadata { 16 + #[serde(skip_serializing_if = "Option::is_none")] 17 + author: Option<String>, 18 + #[serde(skip_serializing_if = "Option::is_none")] 19 + publish_date: Option<String>, 20 + source_url: String, 21 + } 22 + 23 + #[derive(Serialize)] 24 + pub struct ImportArticleResponse { 25 + title: String, 26 + markdown: String, 27 + metadata: ArticleMetadata, 28 } 29 30 pub async fn import_article(Json(payload): Json<ImportRequest>) -> impl IntoResponse { ··· 33 } 34 35 let url = payload.url.clone(); 36 + 37 + // Fetch HTML content 38 + let html_result = reqwest::get(&url).await; 39 + let html_content = match html_result { 40 + Ok(response) => match response.text().await { 41 + Ok(text) => text, 42 + Err(e) => { 43 + return ( 44 + StatusCode::INTERNAL_SERVER_ERROR, 45 + Json(json!({"error": format!("Failed to fetch content: {}", e)})), 46 + ) 47 + .into_response(); 48 + } 49 + }, 50 + Err(e) => { 51 + return ( 52 + StatusCode::INTERNAL_SERVER_ERROR, 53 + Json(json!({"error": format!("Failed to fetch URL: {}", e)})), 54 + ) 55 + .into_response(); 56 + } 57 + }; 58 + 59 + // Extract article using dom_smoothie 60 let url_for_task = url.clone(); 61 + let result = tokio::task::spawn_blocking( 62 + move || -> Result<(String, String, Option<String>, Option<String>), String> { 63 + let mut readability = Readability::new(html_content, Some(&url_for_task), None) 64 + .map_err(|e| format!("Readability error: {}", e))?; 65 + let article = readability.parse().map_err(|e| format!("Parse error: {}", e))?; 66 + Ok(( 67 + article.title, 68 + article.content.to_string(), 69 + article.byline, 70 + article.published_time, 71 + )) 72 + }, 73 + ) 74 + .await; 75 76 + match result { 77 + Ok(Ok((title, content, author, publish_date))) => { 78 + // Convert HTML content to markdown 79 + let markdown = html2md::parse_html(&content); 80 + 81 + let response = ImportArticleResponse { 82 + title, 83 + markdown, 84 + metadata: ArticleMetadata { author, publish_date, source_url: payload.url }, 85 + }; 86 + 87 + Json(response).into_response() 88 + } 89 + Ok(Err(e)) => ( 90 + StatusCode::INTERNAL_SERVER_ERROR, 91 + Json(json!({"error": format!("Failed to extract article: {}", e)})), 92 + ) 93 + .into_response(), 94 + Err(e) => ( 95 + StatusCode::INTERNAL_SERVER_ERROR, 96 + Json(json!({"error": format!("Task join error: {}", e)})), 97 + ) 98 + .into_response(), 99 + } 100 + } 101 + 102 + #[derive(Deserialize)] 103 + pub struct ImportArticleSaveRequest { 104 + url: String, 105 + #[serde(default)] 106 + tags: Vec<String>, 107 + #[serde(default = "default_visibility")] 108 + visibility: Visibility, 109 + } 110 + 111 + fn default_visibility() -> Visibility { 112 + Visibility::Private 113 + } 114 + 115 + pub async fn import_article_save( 116 + Extension(user_ctx): Extension<UserContext>, axum::extract::State(state): axum::extract::State<SharedState>, 117 + Json(payload): Json<ImportArticleSaveRequest>, 118 + ) -> impl IntoResponse { 119 + if payload.url.trim().is_empty() { 120 + return (StatusCode::BAD_REQUEST, Json(json!({"error": "URL is required"}))).into_response(); 121 + } 122 + 123 + let url = payload.url.clone(); 124 + 125 + // Fetch HTML content 126 + let html_result = reqwest::get(&url).await; 127 + let html_content = match html_result { 128 + Ok(response) => match response.text().await { 129 + Ok(text) => text, 130 + Err(e) => { 131 + return ( 132 + StatusCode::INTERNAL_SERVER_ERROR, 133 + Json(json!({"error": format!("Failed to fetch content: {}", e)})), 134 + ) 135 + .into_response(); 136 + } 137 + }, 138 + Err(e) => { 139 + return ( 140 + StatusCode::INTERNAL_SERVER_ERROR, 141 + Json(json!({"error": format!("Failed to fetch URL: {}", e)})), 142 + ) 143 + .into_response(); 144 + } 145 + }; 146 + 147 + // Extract article using dom_smoothie 148 + let url_for_task = url.clone(); 149 + let result = tokio::task::spawn_blocking(move || -> Result<(String, String), String> { 150 + let mut readability = Readability::new(html_content, Some(&url_for_task), None) 151 + .map_err(|e| format!("Readability error: {}", e))?; 152 + let article = readability.parse().map_err(|e| format!("Parse error: {}", e))?; 153 + Ok((article.title, article.content.to_string())) 154 + }) 155 + .await; 156 157 match result { 158 + Ok(Ok((title, content))) => { 159 + // Convert HTML content to markdown 160 + let markdown = html2md::parse_html(&content); 161 + 162 + // Merge auto-tags with user-provided tags 163 + let mut tags = payload.tags.clone(); 164 + if !tags.contains(&"imported".to_string()) { 165 + tags.push("imported".to_string()); 166 + } 167 + if !tags.contains(&"article".to_string()) { 168 + tags.push("article".to_string()); 169 + } 170 + 171 + // Store source URL as first link 172 + let links = vec![payload.url.clone()]; 173 + 174 + // Create note 175 + match state 176 + .note_repo 177 + .create(&user_ctx.did, &title, &markdown, tags, payload.visibility, links) 178 + .await 179 + { 180 + Ok(note) => (StatusCode::CREATED, Json(note)).into_response(), 181 + Err(e) => { 182 + tracing::error!("Failed to create note from import: {:?}", e); 183 + ( 184 + StatusCode::INTERNAL_SERVER_ERROR, 185 + Json(json!({"error": "Failed to save article"})), 186 + ) 187 + .into_response() 188 + } 189 + } 190 + } 191 Ok(Err(e)) => ( 192 StatusCode::INTERNAL_SERVER_ERROR, 193 + Json(json!({"error": format!("Failed to extract article: {}", e)})), 194 ) 195 .into_response(), 196 Err(e) => ( ··· 222 let body_json: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); 223 let title = body_json["title"].as_str().unwrap(); 224 assert!(title.contains("Rust")); 225 + // Verify markdown field exists and is non-empty 226 + let markdown = body_json["markdown"].as_str().unwrap(); 227 + assert!(markdown.len() > 100); 228 + // Verify no HTML tags leak through 229 + assert!(!markdown.contains("<div")); 230 + assert!(!markdown.contains("<p>")); 231 + // Verify metadata structure exists 232 + assert!(body_json["metadata"].is_object()); 233 + assert_eq!( 234 + body_json["metadata"]["source_url"].as_str().unwrap(), 235 + "https://www.rust-lang.org" 236 + ); 237 } 238 239 #[tokio::test]
+1
crates/server/src/api/note.rs
··· 36 &payload.body, 37 payload.tags, 38 payload.visibility, 39 ) 40 .await; 41
··· 36 &payload.body, 37 payload.tags, 38 payload.visibility, 39 + Vec::new(), 40 ) 41 .await; 42
+1
crates/server/src/lib.rs
··· 57 .route("/decks/{id}/fork", post(api::deck::fork_deck)) 58 .route("/notes", post(api::note::create_note)) 59 .route("/cards", post(api::card::create_card)) 60 .route("/review/due", get(api::review::get_due_cards)) 61 .route("/review/submit", post(api::review::submit_review)) 62 .route("/review/stats", get(api::review::get_stats))
··· 57 .route("/decks/{id}/fork", post(api::deck::fork_deck)) 58 .route("/notes", post(api::note::create_note)) 59 .route("/cards", post(api::card::create_card)) 60 + .route("/import/article/save", post(api::importer::import_article_save)) 61 .route("/review/due", get(api::review::get_due_cards)) 62 .route("/review/submit", post(api::review::submit_review)) 63 .route("/review/stats", get(api::review::get_stats))
+26 -8
crates/server/src/repository/note.rs
··· 12 #[async_trait] 13 pub trait NoteRepository: Send + Sync { 14 async fn create( 15 - &self, owner_did: &str, title: &str, body: &str, tags: Vec<String>, visibility: Visibility, 16 ) -> Result<Note, NoteRepoError>; 17 async fn list(&self, viewer_did: Option<&str>) -> Result<Vec<Note>, NoteRepoError>; 18 async fn get(&self, id: &str, viewer_did: Option<&str>) -> Result<Note, NoteRepoError>; ··· 32 #[async_trait] 33 impl NoteRepository for DbNoteRepository { 34 async fn create( 35 - &self, owner_did: &str, title: &str, body: &str, tags: Vec<String>, visibility: Visibility, 36 ) -> Result<Note, NoteRepoError> { 37 let client = self 38 .pool ··· 46 47 client 48 .execute( 49 - "INSERT INTO notes (id, owner_did, title, body, tags, visibility) 50 - VALUES ($1, $2, $3, $4, $5, $6)", 51 - &[&note_id, &owner_did, &title, &body, &tags, &visibility_json], 52 ) 53 .await 54 .map_err(|e| NoteRepoError::DatabaseError(format!("Failed to insert note: {}", e)))?; ··· 61 tags, 62 visibility, 63 published_at: None, 64 - links: Vec::new(), 65 language: None, 66 }) 67 } ··· 257 #[async_trait] 258 impl NoteRepository for MockNoteRepository { 259 async fn create( 260 - &self, owner_did: &str, title: &str, body: &str, tags: Vec<String>, visibility: Visibility, 261 ) -> Result<Note, NoteRepoError> { 262 if *self.should_fail.lock().unwrap() { 263 return Err(NoteRepoError::DatabaseError("Mock failure".to_string())); ··· 271 tags, 272 visibility, 273 published_at: None, 274 - links: Vec::new(), 275 language: None, 276 }; 277
··· 12 #[async_trait] 13 pub trait NoteRepository: Send + Sync { 14 async fn create( 15 + &self, 16 + owner_did: &str, 17 + title: &str, 18 + body: &str, 19 + tags: Vec<String>, 20 + visibility: Visibility, 21 + links: Vec<String>, 22 ) -> Result<Note, NoteRepoError>; 23 async fn list(&self, viewer_did: Option<&str>) -> Result<Vec<Note>, NoteRepoError>; 24 async fn get(&self, id: &str, viewer_did: Option<&str>) -> Result<Note, NoteRepoError>; ··· 38 #[async_trait] 39 impl NoteRepository for DbNoteRepository { 40 async fn create( 41 + &self, 42 + owner_did: &str, 43 + title: &str, 44 + body: &str, 45 + tags: Vec<String>, 46 + visibility: Visibility, 47 + links: Vec<String>, 48 ) -> Result<Note, NoteRepoError> { 49 let client = self 50 .pool ··· 58 59 client 60 .execute( 61 + "INSERT INTO notes (id, owner_did, title, body, tags, visibility, links) 62 + VALUES ($1, $2, $3, $4, $5, $6, $7)", 63 + &[&note_id, &owner_did, &title, &body, &tags, &visibility_json, &links], 64 ) 65 .await 66 .map_err(|e| NoteRepoError::DatabaseError(format!("Failed to insert note: {}", e)))?; ··· 73 tags, 74 visibility, 75 published_at: None, 76 + links, 77 language: None, 78 }) 79 } ··· 269 #[async_trait] 270 impl NoteRepository for MockNoteRepository { 271 async fn create( 272 + &self, 273 + owner_did: &str, 274 + title: &str, 275 + body: &str, 276 + tags: Vec<String>, 277 + visibility: Visibility, 278 + links: Vec<String>, 279 ) -> Result<Note, NoteRepoError> { 280 if *self.should_fail.lock().unwrap() { 281 return Err(NoteRepoError::DatabaseError("Mock failure".to_string())); ··· 289 tags, 290 visibility, 291 published_at: None, 292 + links, 293 language: None, 294 }; 295
+23 -6
web/src/lib/api.ts
··· 58 59 const deck = await res.json(); 60 if (cards && cards.length > 0) { 61 - await Promise.all(cards.map((c) => 62 - apiFetch("/cards", { 63 - method: "POST", 64 - body: JSON.stringify({ deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl }), 65 - }) 66 - )); 67 } 68 69 return { ok: true, json: async () => deck }; ··· 98 return { ok: true }; 99 } 100 return res; 101 }, 102 };
··· 58 59 const deck = await res.json(); 60 if (cards && cards.length > 0) { 61 + await Promise.all( 62 + cards.map((c) => 63 + apiFetch("/cards", { 64 + method: "POST", 65 + body: JSON.stringify({ deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl }), 66 + }) 67 + ), 68 + ); 69 } 70 71 return { ok: true, json: async () => deck }; ··· 100 return { ok: true }; 101 } 102 return res; 103 + }, 104 + // TODO: type visibility 105 + saveImportedArticle: (payload: { url: string; tags?: string[]; visibility?: unknown }) => { 106 + return apiFetch("/import/article/save", { method: "POST", body: JSON.stringify(payload) }); 107 + }, 108 + downloadNoteAsMarkdown: (note: { title: string; body: string }) => { 109 + const blob = new Blob([note.body], { type: "text/markdown" }); 110 + const url = URL.createObjectURL(blob); 111 + const a = document.createElement("a"); 112 + a.href = url; 113 + a.download = `${note.title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md`; 114 + document.body.appendChild(a); 115 + a.click(); 116 + document.body.removeChild(a); 117 + URL.revokeObjectURL(url); 118 }, 119 };
+67 -7
web/src/pages/Import.tsx
··· 2 import { api } from "$lib/api"; 3 import { toast } from "$lib/toast"; 4 import { Button } from "$ui/Button"; 5 import { createSignal, Show } from "solid-js"; 6 7 export default function Import() { 8 const [url, setUrl] = createSignal(""); 9 const [loading, setLoading] = createSignal(false); 10 - const [importedData, setImportedData] = createSignal<{ title: string; content: string } | null>(null); 11 12 const handleImport = async (e: Event) => { 13 e.preventDefault(); 14 setLoading(true); 15 setImportedData(null); 16 try { 17 const res = await api.post("/import/article", { url: url() }); 18 if (res.ok) { 19 const data = await res.json(); 20 - const content = `Source: [${data.title}](${data.url})\n\n${data.text}`; 21 - setImportedData({ title: data.title, content }); 22 - toast.success("Article imported!"); 23 } else { 24 const err = await res.json(); 25 toast.error(err.error || "Failed to import"); ··· 32 } 33 }; 34 35 return ( 36 <div class="max-w-4xl mx-auto space-y-8"> 37 <div class="space-y-2"> ··· 50 <Button type="submit" disabled={loading()}>{loading() ? "Importing..." : "Import"}</Button> 51 </form> 52 53 - <Show when={importedData()}> 54 <div class="pt-8 border-t border-gray-800"> 55 - <h2 class="text-xl font-semibold text-white mb-4">Create Note from Import</h2> 56 - <NoteEditor initialTitle={importedData()?.title} initialContent={importedData()?.content} /> 57 </div> 58 </Show> 59 </div>
··· 2 import { api } from "$lib/api"; 3 import { toast } from "$lib/toast"; 4 import { Button } from "$ui/Button"; 5 + import { useNavigate } from "@solidjs/router"; 6 import { createSignal, Show } from "solid-js"; 7 8 export default function Import() { 9 + const navigate = useNavigate(); 10 const [url, setUrl] = createSignal(""); 11 const [loading, setLoading] = createSignal(false); 12 + const [saving, setSaving] = createSignal(false); 13 + const [showEditor, setShowEditor] = createSignal(false); 14 + const [importedData, setImportedData] = createSignal< 15 + { title: string; markdown: string; metadata: { author?: string; publish_date?: string; source_url: string } } | null 16 + >(null); 17 18 const handleImport = async (e: Event) => { 19 e.preventDefault(); 20 setLoading(true); 21 setImportedData(null); 22 + setShowEditor(false); 23 try { 24 const res = await api.post("/import/article", { url: url() }); 25 if (res.ok) { 26 const data = await res.json(); 27 + setImportedData({ title: data.title, markdown: data.markdown, metadata: data.metadata }); 28 + toast.success("Article extracted!"); 29 } else { 30 const err = await res.json(); 31 toast.error(err.error || "Failed to import"); ··· 38 } 39 }; 40 41 + const handleQuickSave = async () => { 42 + if (!importedData()) return; 43 + setSaving(true); 44 + try { 45 + const res = await api.saveImportedArticle({ 46 + url: importedData()!.metadata.source_url, 47 + tags: ["imported", "article"], 48 + visibility: { type: "Private" }, 49 + }); 50 + if (res.ok) { 51 + const note = await res.json(); 52 + toast.success("Article saved!"); 53 + navigate(`/notes/${note.id}`); 54 + } else { 55 + const err = await res.json(); 56 + toast.error(err.error || "Failed to save"); 57 + } 58 + } catch (e) { 59 + console.error(e); 60 + toast.error("Network error"); 61 + } finally { 62 + setSaving(false); 63 + } 64 + }; 65 + 66 return ( 67 <div class="max-w-4xl mx-auto space-y-8"> 68 <div class="space-y-2"> ··· 81 <Button type="submit" disabled={loading()}>{loading() ? "Importing..." : "Import"}</Button> 82 </form> 83 84 + <Show when={importedData() && !showEditor()}> 85 + <div class="pt-8 border-t border-gray-800 space-y-4"> 86 + <div> 87 + <h2 class="text-2xl font-semibold text-white">{importedData()!.title}</h2> 88 + <Show when={importedData()!.metadata.author || importedData()!.metadata.publish_date}> 89 + <div class="flex gap-4 text-sm text-[#C6C6C6] mt-2"> 90 + <Show when={importedData()!.metadata.author}> 91 + <span>By {importedData()!.metadata.author}</span> 92 + </Show> 93 + <Show when={importedData()!.metadata.publish_date}> 94 + <span>Published: {new Date(importedData()!.metadata.publish_date!).toLocaleDateString()}</span> 95 + </Show> 96 + </div> 97 + </Show> 98 + </div> 99 + 100 + <div class="bg-gray-800 rounded-lg p-4 max-h-96 overflow-y-auto"> 101 + <p class="text-[#C6C6C6] text-sm whitespace-pre-wrap line-clamp-6"> 102 + {importedData()!.markdown.slice(0, 500)}... 103 + </p> 104 + </div> 105 + 106 + <div class="flex gap-4"> 107 + <Button onClick={handleQuickSave} disabled={saving()}>{saving() ? "Saving..." : "Quick Save"}</Button> 108 + <Button variant="secondary" onClick={() => setShowEditor(true)}>Edit First</Button> 109 + </div> 110 + </div> 111 + </Show> 112 + 113 + <Show when={showEditor() && importedData()}> 114 <div class="pt-8 border-t border-gray-800"> 115 + <h2 class="text-xl font-semibold text-white mb-4">Edit Before Saving</h2> 116 + <NoteEditor initialTitle={importedData()?.title} initialContent={importedData()?.markdown} /> 117 </div> 118 </Show> 119 </div>
+3 -8
web/src/pages/NoteView.tsx
··· 5 import { Button } from "$components/ui/Button"; 6 import { api } from "$lib/api"; 7 import type { Note } from "$lib/model"; 8 - import { 9 - extractHeadings, 10 - findBacklinks, 11 - type Heading, 12 - parseWikilinks, 13 - resolveWikilink, 14 - type WikiLink, 15 - } from "$lib/wikilink"; 16 import { Tag } from "$ui/Tag"; 17 import rehypeShiki from "@shikijs/rehype"; 18 import { A, useParams } from "@solidjs/router"; ··· 118 <A href={`/notes/edit/${n().id}`}> 119 <Button variant="secondary">Edit</Button> 120 </A> 121 </div> 122 </header> 123
··· 5 import { Button } from "$components/ui/Button"; 6 import { api } from "$lib/api"; 7 import type { Note } from "$lib/model"; 8 + import { extractHeadings, findBacklinks, parseWikilinks, resolveWikilink } from "$lib/wikilink"; 9 + import type { Heading, WikiLink } from "$lib/wikilink"; 10 import { Tag } from "$ui/Tag"; 11 import rehypeShiki from "@shikijs/rehype"; 12 import { A, useParams } from "@solidjs/router"; ··· 112 <A href={`/notes/edit/${n().id}`}> 113 <Button variant="secondary">Edit</Button> 114 </A> 115 + <Button variant="secondary" onClick={() => api.downloadNoteAsMarkdown(n())}>Download Markdown</Button> 116 </div> 117 </header> 118