+350
-270
Cargo.lock
+350
-270
Cargo.lock
···
116
116
"p256",
117
117
"p384",
118
118
"rand 0.8.5",
119
-
"reqwest 0.12.28",
119
+
"reqwest",
120
120
"serde",
121
121
"serde_ipld_dagcbor",
122
122
"serde_json",
···
136
136
"async-trait",
137
137
"atproto-identity",
138
138
"futures",
139
-
"http 1.4.0",
139
+
"http",
140
140
"serde",
141
141
"serde_json",
142
142
"thiserror 2.0.17",
···
165
165
"bytes",
166
166
"form_urlencoded",
167
167
"futures-util",
168
-
"http 1.4.0",
169
-
"http-body 1.0.1",
168
+
"http",
169
+
"http-body",
170
170
"http-body-util",
171
-
"hyper 1.8.1",
171
+
"hyper",
172
172
"hyper-util",
173
173
"itoa",
174
174
"matchit",
···
180
180
"serde_json",
181
181
"serde_path_to_error",
182
182
"serde_urlencoded",
183
-
"sync_wrapper 1.0.2",
183
+
"sync_wrapper",
184
184
"tokio",
185
185
"tower",
186
186
"tower-layer",
···
196
196
dependencies = [
197
197
"bytes",
198
198
"futures-core",
199
-
"http 1.4.0",
200
-
"http-body 1.0.1",
199
+
"http",
200
+
"http-body",
201
201
"http-body-util",
202
202
"mime",
203
203
"pin-project-lite",
204
-
"sync_wrapper 1.0.2",
204
+
"sync_wrapper",
205
205
"tower-layer",
206
206
"tower-service",
207
207
"tracing",
···
231
231
232
232
[[package]]
233
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
234
version = "0.22.1"
241
235
source = "registry+https://github.com/rust-lang/crates.io-index"
242
236
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
···
246
240
version = "1.8.1"
247
241
source = "registry+https://github.com/rust-lang/crates.io-index"
248
242
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
243
256
244
[[package]]
257
245
name = "bitflags"
···
306
294
"libc",
307
295
"shlex",
308
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"
309
303
310
304
[[package]]
311
305
name = "cfg-if"
···
394
388
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
395
389
396
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]]
397
401
name = "const-oid"
398
402
version = "0.9.6"
399
403
source = "registry+https://github.com/rust-lang/crates.io-index"
···
513
517
]
514
518
515
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]]
516
543
name = "curve25519-dalek"
517
544
version = "4.1.3"
518
545
source = "registry+https://github.com/rust-lang/crates.io-index"
···
621
648
]
622
649
623
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]]
624
662
name = "digest"
625
663
version = "0.10.7"
626
664
source = "registry+https://github.com/rust-lang/crates.io-index"
···
644
682
]
645
683
646
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]]
647
718
name = "dotenvy"
648
719
version = "0.15.7"
649
720
source = "registry+https://github.com/rust-lang/crates.io-index"
650
721
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
651
722
652
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]]
653
739
name = "ecdsa"
654
740
version = "0.16.9"
655
741
source = "registry+https://github.com/rust-lang/crates.io-index"
···
784
870
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
785
871
786
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]]
787
879
name = "fnv"
788
880
version = "1.0.7"
789
881
source = "registry+https://github.com/rust-lang/crates.io-index"
···
919
1011
]
920
1012
921
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]]
922
1023
name = "generic-array"
923
1024
version = "0.14.7"
924
1025
source = "registry+https://github.com/rust-lang/crates.io-index"
···
957
1058
]
958
1059
959
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]]
960
1067
name = "group"
961
1068
version = "0.13.0"
962
1069
source = "registry+https://github.com/rust-lang/crates.io-index"
···
969
1076
970
1077
[[package]]
971
1078
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
1079
version = "0.4.12"
992
1080
source = "registry+https://github.com/rust-lang/crates.io-index"
993
1081
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
···
997
1085
"fnv",
998
1086
"futures-core",
999
1087
"futures-sink",
1000
-
"http 1.4.0",
1088
+
"http",
1001
1089
"indexmap",
1002
1090
"slab",
1003
1091
"tokio",
···
1144
1232
]
1145
1233
1146
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]]
1147
1258
name = "html5ever"
1148
-
version = "0.26.0"
1259
+
version = "0.27.0"
1149
1260
source = "registry+https://github.com/rust-lang/crates.io-index"
1150
-
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
1261
+
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
1151
1262
dependencies = [
1152
1263
"log",
1153
1264
"mac",
1154
-
"markup5ever",
1265
+
"markup5ever 0.12.1",
1155
1266
"proc-macro2",
1156
1267
"quote",
1157
-
"syn 1.0.109",
1268
+
"syn 2.0.111",
1158
1269
]
1159
1270
1160
1271
[[package]]
1161
-
name = "http"
1162
-
version = "0.2.12"
1272
+
name = "html5ever"
1273
+
version = "0.29.1"
1163
1274
source = "registry+https://github.com/rust-lang/crates.io-index"
1164
-
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
1275
+
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
1165
1276
dependencies = [
1166
-
"bytes",
1167
-
"fnv",
1168
-
"itoa",
1277
+
"log",
1278
+
"mac",
1279
+
"markup5ever 0.14.1",
1280
+
"match_token",
1169
1281
]
1170
1282
1171
1283
[[package]]
···
1180
1292
1181
1293
[[package]]
1182
1294
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
1295
version = "1.0.1"
1195
1296
source = "registry+https://github.com/rust-lang/crates.io-index"
1196
1297
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
1197
1298
dependencies = [
1198
1299
"bytes",
1199
-
"http 1.4.0",
1300
+
"http",
1200
1301
]
1201
1302
1202
1303
[[package]]
···
1207
1308
dependencies = [
1208
1309
"bytes",
1209
1310
"futures-core",
1210
-
"http 1.4.0",
1211
-
"http-body 1.0.1",
1311
+
"http",
1312
+
"http-body",
1212
1313
"pin-project-lite",
1213
1314
]
1214
1315
···
1226
1327
1227
1328
[[package]]
1228
1329
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
1330
version = "1.8.1"
1254
1331
source = "registry+https://github.com/rust-lang/crates.io-index"
1255
1332
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
···
1258
1335
"bytes",
1259
1336
"futures-channel",
1260
1337
"futures-core",
1261
-
"h2 0.4.12",
1262
-
"http 1.4.0",
1263
-
"http-body 1.0.1",
1338
+
"h2",
1339
+
"http",
1340
+
"http-body",
1264
1341
"httparse",
1265
1342
"httpdate",
1266
1343
"itoa",
···
1277
1354
source = "registry+https://github.com/rust-lang/crates.io-index"
1278
1355
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
1279
1356
dependencies = [
1280
-
"http 1.4.0",
1281
-
"hyper 1.8.1",
1357
+
"http",
1358
+
"hyper",
1282
1359
"hyper-util",
1283
1360
"rustls",
1284
1361
"rustls-pki-types",
···
1290
1367
1291
1368
[[package]]
1292
1369
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
1370
version = "0.6.0"
1307
1371
source = "registry+https://github.com/rust-lang/crates.io-index"
1308
1372
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
1309
1373
dependencies = [
1310
1374
"bytes",
1311
1375
"http-body-util",
1312
-
"hyper 1.8.1",
1376
+
"hyper",
1313
1377
"hyper-util",
1314
1378
"native-tls",
1315
1379
"tokio",
···
1323
1387
source = "registry+https://github.com/rust-lang/crates.io-index"
1324
1388
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
1325
1389
dependencies = [
1326
-
"base64 0.22.1",
1390
+
"base64",
1327
1391
"bytes",
1328
1392
"futures-channel",
1329
1393
"futures-core",
1330
1394
"futures-util",
1331
-
"http 1.4.0",
1332
-
"http-body 1.0.1",
1333
-
"hyper 1.8.1",
1395
+
"http",
1396
+
"http-body",
1397
+
"hyper",
1334
1398
"ipnet",
1335
1399
"libc",
1336
1400
"percent-encoding",
1337
1401
"pin-project-lite",
1338
1402
"socket2 0.6.1",
1339
-
"system-configuration 0.6.1",
1403
+
"system-configuration",
1340
1404
"tokio",
1341
1405
"tower-service",
1342
1406
"tracing",
···
1529
1593
version = "1.0.17"
1530
1594
source = "registry+https://github.com/rust-lang/crates.io-index"
1531
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"
1532
1616
1533
1617
[[package]]
1534
1618
name = "jobserver"
···
1582
1666
source = "registry+https://github.com/rust-lang/crates.io-index"
1583
1667
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
1584
1668
dependencies = [
1585
-
"bitflags 2.10.0",
1669
+
"bitflags",
1586
1670
"libc",
1587
1671
"redox_syscall 0.7.0",
1588
1672
]
···
1656
1740
dependencies = [
1657
1741
"chrono",
1658
1742
"clap",
1743
+
"dom_smoothie",
1659
1744
"dotenvy",
1745
+
"html2md",
1660
1746
"malfestio-core",
1661
1747
"malfestio-server",
1748
+
"reqwest",
1662
1749
"tokio",
1663
1750
"tokio-postgres",
1664
1751
]
···
1681
1768
"async-trait",
1682
1769
"atproto-jetstream",
1683
1770
"axum",
1684
-
"base64 0.22.1",
1771
+
"base64",
1685
1772
"chrono",
1686
1773
"deadpool-postgres",
1774
+
"dom_smoothie",
1687
1775
"dotenvy",
1688
1776
"ed25519-dalek",
1689
1777
"getrandom 0.3.4",
1690
1778
"hickory-resolver 0.24.4",
1779
+
"html2md",
1691
1780
"malfestio-core",
1692
-
"readability",
1693
1781
"regex",
1694
-
"reqwest 0.12.28",
1782
+
"reqwest",
1695
1783
"serde",
1696
1784
"serde_json",
1697
1785
"serde_qs",
···
1710
1798
1711
1799
[[package]]
1712
1800
name = "markup5ever"
1713
-
version = "0.11.0"
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"
1714
1816
source = "registry+https://github.com/rust-lang/crates.io-index"
1715
-
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
1817
+
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
1716
1818
dependencies = [
1717
1819
"log",
1718
-
"phf 0.10.1",
1820
+
"phf 0.11.3",
1719
1821
"phf_codegen",
1720
1822
"string_cache",
1721
1823
"string_cache_codegen",
···
1724
1826
1725
1827
[[package]]
1726
1828
name = "markup5ever_rcdom"
1727
-
version = "0.2.0"
1829
+
version = "0.3.0"
1728
1830
source = "registry+https://github.com/rust-lang/crates.io-index"
1729
-
checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2"
1831
+
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
1730
1832
dependencies = [
1731
-
"html5ever",
1732
-
"markup5ever",
1833
+
"html5ever 0.27.0",
1834
+
"markup5ever 0.12.1",
1733
1835
"tendril",
1734
1836
"xml5ever",
1735
1837
]
···
1743
1845
"proc-macro2",
1744
1846
"quote",
1745
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",
1746
1859
]
1747
1860
1748
1861
[[package]]
···
1912
2025
source = "registry+https://github.com/rust-lang/crates.io-index"
1913
2026
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
1914
2027
dependencies = [
1915
-
"bitflags 2.10.0",
2028
+
"bitflags",
1916
2029
"cfg-if",
1917
2030
"foreign-types",
1918
2031
"libc",
···
2014
2127
2015
2128
[[package]]
2016
2129
name = "phf"
2017
-
version = "0.10.1"
2130
+
version = "0.11.3"
2018
2131
source = "registry+https://github.com/rust-lang/crates.io-index"
2019
-
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
2132
+
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
2020
2133
dependencies = [
2021
-
"phf_shared 0.10.0",
2134
+
"phf_macros",
2135
+
"phf_shared 0.11.3",
2022
2136
]
2023
2137
2024
2138
[[package]]
···
2033
2147
2034
2148
[[package]]
2035
2149
name = "phf_codegen"
2036
-
version = "0.10.0"
2150
+
version = "0.11.3"
2037
2151
source = "registry+https://github.com/rust-lang/crates.io-index"
2038
-
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
2152
+
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
2039
2153
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",
2154
+
"phf_generator",
2155
+
"phf_shared 0.11.3",
2052
2156
]
2053
2157
2054
2158
[[package]]
···
2062
2166
]
2063
2167
2064
2168
[[package]]
2065
-
name = "phf_shared"
2066
-
version = "0.10.0"
2169
+
name = "phf_macros"
2170
+
version = "0.11.3"
2067
2171
source = "registry+https://github.com/rust-lang/crates.io-index"
2068
-
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
2172
+
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
2069
2173
dependencies = [
2070
-
"siphasher 0.3.11",
2174
+
"phf_generator",
2175
+
"phf_shared 0.11.3",
2176
+
"proc-macro2",
2177
+
"quote",
2178
+
"syn 2.0.111",
2071
2179
]
2072
2180
2073
2181
[[package]]
···
2076
2184
source = "registry+https://github.com/rust-lang/crates.io-index"
2077
2185
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
2078
2186
dependencies = [
2079
-
"siphasher 1.0.1",
2187
+
"siphasher",
2080
2188
]
2081
2189
2082
2190
[[package]]
···
2085
2193
source = "registry+https://github.com/rust-lang/crates.io-index"
2086
2194
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
2087
2195
dependencies = [
2088
-
"siphasher 1.0.1",
2196
+
"siphasher",
2089
2197
]
2090
2198
2091
2199
[[package]]
···
2128
2236
source = "registry+https://github.com/rust-lang/crates.io-index"
2129
2237
checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4"
2130
2238
dependencies = [
2131
-
"base64 0.22.1",
2239
+
"base64",
2132
2240
"byteorder",
2133
2241
"bytes",
2134
2242
"fallible-iterator",
···
2333
2441
]
2334
2442
2335
2443
[[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
2444
name = "redox_syscall"
2351
2445
version = "0.5.18"
2352
2446
source = "registry+https://github.com/rust-lang/crates.io-index"
2353
2447
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
2354
2448
dependencies = [
2355
-
"bitflags 2.10.0",
2449
+
"bitflags",
2356
2450
]
2357
2451
2358
2452
[[package]]
···
2361
2455
source = "registry+https://github.com/rust-lang/crates.io-index"
2362
2456
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
2363
2457
dependencies = [
2364
-
"bitflags 2.10.0",
2458
+
"bitflags",
2365
2459
]
2366
2460
2367
2461
[[package]]
···
2395
2489
2396
2490
[[package]]
2397
2491
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
2492
version = "0.12.28"
2439
2493
source = "registry+https://github.com/rust-lang/crates.io-index"
2440
2494
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
2441
2495
dependencies = [
2442
-
"base64 0.22.1",
2496
+
"base64",
2443
2497
"bytes",
2444
2498
"encoding_rs",
2445
2499
"futures-core",
2446
-
"h2 0.4.12",
2447
-
"http 1.4.0",
2448
-
"http-body 1.0.1",
2500
+
"h2",
2501
+
"http",
2502
+
"http-body",
2449
2503
"http-body-util",
2450
-
"hyper 1.8.1",
2504
+
"hyper",
2451
2505
"hyper-rustls",
2452
-
"hyper-tls 0.6.0",
2506
+
"hyper-tls",
2453
2507
"hyper-util",
2454
2508
"js-sys",
2455
2509
"log",
···
2463
2517
"serde",
2464
2518
"serde_json",
2465
2519
"serde_urlencoded",
2466
-
"sync_wrapper 1.0.2",
2520
+
"sync_wrapper",
2467
2521
"tokio",
2468
2522
"tokio-native-tls",
2469
2523
"tokio-rustls",
···
2528
2582
source = "registry+https://github.com/rust-lang/crates.io-index"
2529
2583
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
2530
2584
dependencies = [
2531
-
"bitflags 2.10.0",
2585
+
"bitflags",
2532
2586
"errno",
2533
2587
"libc",
2534
2588
"linux-raw-sys",
···
2562
2616
]
2563
2617
2564
2618
[[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
2619
name = "rustls-pki-types"
2575
2620
version = "1.13.2"
2576
2621
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2604
2649
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
2605
2650
2606
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]]
2607
2661
name = "schannel"
2608
2662
version = "0.1.28"
2609
2663
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2639
2693
source = "registry+https://github.com/rust-lang/crates.io-index"
2640
2694
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
2641
2695
dependencies = [
2642
-
"bitflags 2.10.0",
2696
+
"bitflags",
2643
2697
"core-foundation 0.9.4",
2644
2698
"core-foundation-sys",
2645
2699
"libc",
···
2652
2706
source = "registry+https://github.com/rust-lang/crates.io-index"
2653
2707
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
2654
2708
dependencies = [
2655
-
"bitflags 2.10.0",
2709
+
"bitflags",
2656
2710
"core-foundation 0.10.1",
2657
2711
"core-foundation-sys",
2658
2712
"libc",
···
2670
2724
]
2671
2725
2672
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]]
2673
2746
name = "semver"
2674
2747
version = "1.0.27"
2675
2748
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2785
2858
]
2786
2859
2787
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]]
2788
2870
name = "sha2"
2789
2871
version = "0.10.9"
2790
2872
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2838
2920
2839
2921
[[package]]
2840
2922
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
2923
version = "1.0.1"
2848
2924
source = "registry+https://github.com/rust-lang/crates.io-index"
2849
2925
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
···
2915
2991
source = "registry+https://github.com/rust-lang/crates.io-index"
2916
2992
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
2917
2993
dependencies = [
2918
-
"phf_generator 0.11.3",
2994
+
"phf_generator",
2919
2995
"phf_shared 0.11.3",
2920
2996
"proc-macro2",
2921
2997
"quote",
···
2968
3044
2969
3045
[[package]]
2970
3046
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
3047
version = "1.0.2"
2978
3048
source = "registry+https://github.com/rust-lang/crates.io-index"
2979
3049
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
···
2994
3064
2995
3065
[[package]]
2996
3066
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
3067
version = "0.6.1"
3009
3068
source = "registry+https://github.com/rust-lang/crates.io-index"
3010
3069
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
3011
3070
dependencies = [
3012
-
"bitflags 2.10.0",
3071
+
"bitflags",
3013
3072
"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",
3073
+
"system-configuration-sys",
3025
3074
]
3026
3075
3027
3076
[[package]]
···
3263
3312
source = "registry+https://github.com/rust-lang/crates.io-index"
3264
3313
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
3265
3314
dependencies = [
3266
-
"base64 0.22.1",
3315
+
"base64",
3267
3316
"bytes",
3268
3317
"futures-core",
3269
3318
"futures-sink",
3270
-
"http 1.4.0",
3319
+
"http",
3271
3320
"httparse",
3272
3321
"rand 0.9.2",
3273
3322
"ring",
···
3288
3337
"futures-core",
3289
3338
"futures-util",
3290
3339
"pin-project-lite",
3291
-
"sync_wrapper 1.0.2",
3340
+
"sync_wrapper",
3292
3341
"tokio",
3293
3342
"tower-layer",
3294
3343
"tower-service",
···
3304
3353
"axum-core",
3305
3354
"cookie",
3306
3355
"futures-util",
3307
-
"http 1.4.0",
3356
+
"http",
3308
3357
"parking_lot",
3309
3358
"pin-project-lite",
3310
3359
"tower-layer",
···
3317
3366
source = "registry+https://github.com/rust-lang/crates.io-index"
3318
3367
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
3319
3368
dependencies = [
3320
-
"bitflags 2.10.0",
3369
+
"bitflags",
3321
3370
"bytes",
3322
3371
"futures-util",
3323
-
"http 1.4.0",
3324
-
"http-body 1.0.1",
3372
+
"http",
3373
+
"http-body",
3325
3374
"iri-string",
3326
3375
"pin-project-lite",
3327
3376
"tower",
···
3444
3493
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
3445
3494
3446
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]]
3447
3502
name = "unsigned-varint"
3448
3503
version = "0.8.0"
3449
3504
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3480
3535
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
3481
3536
3482
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]]
3483
3544
name = "utf8_iter"
3484
3545
version = "1.0.4"
3485
3546
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3522
3583
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
3523
3584
3524
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]]
3525
3596
name = "want"
3526
3597
version = "0.3.1"
3527
3598
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3654
3725
version = "1.2.1"
3655
3726
source = "registry+https://github.com/rust-lang/crates.io-index"
3656
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
+
]
3657
3737
3658
3738
[[package]]
3659
3739
name = "windows-core"
···
3971
4051
3972
4052
[[package]]
3973
4053
name = "xml5ever"
3974
-
version = "0.17.0"
4054
+
version = "0.18.1"
3975
4055
source = "registry+https://github.com/rust-lang/crates.io-index"
3976
-
checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650"
4056
+
checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
3977
4057
dependencies = [
3978
4058
"log",
3979
4059
"mac",
3980
-
"markup5ever",
4060
+
"markup5ever 0.12.1",
3981
4061
]
3982
4062
3983
4063
[[package]]
+3
crates/cli/Cargo.toml
+3
crates/cli/Cargo.toml
···
6
6
[dependencies]
7
7
chrono = "0.4"
8
8
clap = { version = "4.5.53", features = ["derive"] }
9
+
dom_smoothie = "0.4"
9
10
dotenvy = "0.15.7"
11
+
html2md = "0.2.15"
10
12
malfestio-core = { version = "0.1.0", path = "../core" }
11
13
malfestio-server = { version = "0.1.0", path = "../server" }
14
+
reqwest = { version = "0.12", features = ["json"] }
12
15
tokio = { version = "1.48.0", features = ["full"] }
13
16
tokio-postgres = "0.7.13"
+123
crates/cli/src/main.rs
+123
crates/cli/src/main.rs
···
28
28
/// Bluesky handle to test (e.g., alice.bsky.social)
29
29
handle: String,
30
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
+
},
31
50
}
32
51
33
52
#[tokio::main]
···
47
66
Commands::Check { handle } => {
48
67
check_flow(handle).await?;
49
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
+
},
50
75
}
51
76
52
77
Ok(())
···
289
314
format!("{} months ago", duration.num_days() / 30)
290
315
}
291
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
+4
-3
crates/server/Cargo.toml
···
13
13
deadpool-postgres = "0.14.0"
14
14
dotenvy = "0.15.7"
15
15
ed25519-dalek = { version = "2.2.0", features = ["serde"] }
16
+
dom_smoothie = "0.4"
16
17
getrandom = { version = "0.3", features = ["std"] }
18
+
html2md = "0.2.15"
17
19
malfestio-core = { version = "0.1.0", path = "../core" }
18
-
readability = "0.3.0"
19
20
regex = "1.12.2"
20
-
reqwest = { version = "0.12.28", features = ["json"] }
21
+
reqwest = { version = "0.12", features = ["json"] }
21
22
serde = "1.0.228"
22
23
serde_json = "1.0.148"
23
24
serde_qs = "0.15"
···
36
37
tracing = "0.1.44"
37
38
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
38
39
uuid = { version = "1.19.0", features = ["v4", "fast-rng"] }
39
-
hickory-resolver = "0.24.0"
40
+
hickory-resolver = "0.24"
+186
-13
crates/server/src/api/importer.rs
+186
-13
crates/server/src/api/importer.rs
···
1
-
use axum::{Json, http::StatusCode, response::IntoResponse};
2
-
use readability::extractor;
3
-
use serde::Deserialize;
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};
4
7
use serde_json::json;
5
8
6
9
#[derive(Deserialize)]
7
10
pub struct ImportRequest {
8
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,
9
28
}
10
29
11
30
pub async fn import_article(Json(payload): Json<ImportRequest>) -> impl IntoResponse {
···
14
33
}
15
34
16
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
17
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;
18
75
19
-
let result = tokio::task::spawn_blocking(move || extractor::scrape(&url_for_task)).await;
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;
20
156
21
157
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(),
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
+
}
29
191
Ok(Err(e)) => (
30
192
StatusCode::INTERNAL_SERVER_ERROR,
31
-
Json(json!({"error": format!("Readability error: {}", e)})),
193
+
Json(json!({"error": format!("Failed to extract article: {}", e)})),
32
194
)
33
195
.into_response(),
34
196
Err(e) => (
···
60
222
let body_json: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
61
223
let title = body_json["title"].as_str().unwrap();
62
224
assert!(title.contains("Rust"));
63
-
assert!(body_json["text"].as_str().unwrap().len() > 100);
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
+
);
64
237
}
65
238
66
239
#[tokio::test]
+1
crates/server/src/api/note.rs
+1
crates/server/src/api/note.rs
+1
crates/server/src/lib.rs
+1
crates/server/src/lib.rs
···
57
57
.route("/decks/{id}/fork", post(api::deck::fork_deck))
58
58
.route("/notes", post(api::note::create_note))
59
59
.route("/cards", post(api::card::create_card))
60
+
.route("/import/article/save", post(api::importer::import_article_save))
60
61
.route("/review/due", get(api::review::get_due_cards))
61
62
.route("/review/submit", post(api::review::submit_review))
62
63
.route("/review/stats", get(api::review::get_stats))
+26
-8
crates/server/src/repository/note.rs
+26
-8
crates/server/src/repository/note.rs
···
12
12
#[async_trait]
13
13
pub trait NoteRepository: Send + Sync {
14
14
async fn create(
15
-
&self, owner_did: &str, title: &str, body: &str, tags: Vec<String>, visibility: Visibility,
15
+
&self,
16
+
owner_did: &str,
17
+
title: &str,
18
+
body: &str,
19
+
tags: Vec<String>,
20
+
visibility: Visibility,
21
+
links: Vec<String>,
16
22
) -> Result<Note, NoteRepoError>;
17
23
async fn list(&self, viewer_did: Option<&str>) -> Result<Vec<Note>, NoteRepoError>;
18
24
async fn get(&self, id: &str, viewer_did: Option<&str>) -> Result<Note, NoteRepoError>;
···
32
38
#[async_trait]
33
39
impl NoteRepository for DbNoteRepository {
34
40
async fn create(
35
-
&self, owner_did: &str, title: &str, body: &str, tags: Vec<String>, visibility: Visibility,
41
+
&self,
42
+
owner_did: &str,
43
+
title: &str,
44
+
body: &str,
45
+
tags: Vec<String>,
46
+
visibility: Visibility,
47
+
links: Vec<String>,
36
48
) -> Result<Note, NoteRepoError> {
37
49
let client = self
38
50
.pool
···
46
58
47
59
client
48
60
.execute(
49
-
"INSERT INTO notes (id, owner_did, title, body, tags, visibility)
50
-
VALUES ($1, $2, $3, $4, $5, $6)",
51
-
&[¬e_id, &owner_did, &title, &body, &tags, &visibility_json],
61
+
"INSERT INTO notes (id, owner_did, title, body, tags, visibility, links)
62
+
VALUES ($1, $2, $3, $4, $5, $6, $7)",
63
+
&[¬e_id, &owner_did, &title, &body, &tags, &visibility_json, &links],
52
64
)
53
65
.await
54
66
.map_err(|e| NoteRepoError::DatabaseError(format!("Failed to insert note: {}", e)))?;
···
61
73
tags,
62
74
visibility,
63
75
published_at: None,
64
-
links: Vec::new(),
76
+
links,
65
77
language: None,
66
78
})
67
79
}
···
257
269
#[async_trait]
258
270
impl NoteRepository for MockNoteRepository {
259
271
async fn create(
260
-
&self, owner_did: &str, title: &str, body: &str, tags: Vec<String>, visibility: Visibility,
272
+
&self,
273
+
owner_did: &str,
274
+
title: &str,
275
+
body: &str,
276
+
tags: Vec<String>,
277
+
visibility: Visibility,
278
+
links: Vec<String>,
261
279
) -> Result<Note, NoteRepoError> {
262
280
if *self.should_fail.lock().unwrap() {
263
281
return Err(NoteRepoError::DatabaseError("Mock failure".to_string()));
···
271
289
tags,
272
290
visibility,
273
291
published_at: None,
274
-
links: Vec::new(),
292
+
links,
275
293
language: None,
276
294
};
277
295
+23
-6
web/src/lib/api.ts
+23
-6
web/src/lib/api.ts
···
58
58
59
59
const deck = await res.json();
60
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
-
));
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
+
);
67
69
}
68
70
69
71
return { ok: true, json: async () => deck };
···
98
100
return { ok: true };
99
101
}
100
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);
101
118
},
102
119
};
+67
-7
web/src/pages/Import.tsx
+67
-7
web/src/pages/Import.tsx
···
2
2
import { api } from "$lib/api";
3
3
import { toast } from "$lib/toast";
4
4
import { Button } from "$ui/Button";
5
+
import { useNavigate } from "@solidjs/router";
5
6
import { createSignal, Show } from "solid-js";
6
7
7
8
export default function Import() {
9
+
const navigate = useNavigate();
8
10
const [url, setUrl] = createSignal("");
9
11
const [loading, setLoading] = createSignal(false);
10
-
const [importedData, setImportedData] = createSignal<{ title: string; content: string } | null>(null);
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);
11
17
12
18
const handleImport = async (e: Event) => {
13
19
e.preventDefault();
14
20
setLoading(true);
15
21
setImportedData(null);
22
+
setShowEditor(false);
16
23
try {
17
24
const res = await api.post("/import/article", { url: url() });
18
25
if (res.ok) {
19
26
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!");
27
+
setImportedData({ title: data.title, markdown: data.markdown, metadata: data.metadata });
28
+
toast.success("Article extracted!");
23
29
} else {
24
30
const err = await res.json();
25
31
toast.error(err.error || "Failed to import");
···
32
38
}
33
39
};
34
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
+
35
66
return (
36
67
<div class="max-w-4xl mx-auto space-y-8">
37
68
<div class="space-y-2">
···
50
81
<Button type="submit" disabled={loading()}>{loading() ? "Importing..." : "Import"}</Button>
51
82
</form>
52
83
53
-
<Show when={importedData()}>
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()}>
54
114
<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} />
115
+
<h2 class="text-xl font-semibold text-white mb-4">Edit Before Saving</h2>
116
+
<NoteEditor initialTitle={importedData()?.title} initialContent={importedData()?.markdown} />
57
117
</div>
58
118
</Show>
59
119
</div>
+3
-8
web/src/pages/NoteView.tsx
+3
-8
web/src/pages/NoteView.tsx
···
5
5
import { Button } from "$components/ui/Button";
6
6
import { api } from "$lib/api";
7
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";
8
+
import { extractHeadings, findBacklinks, parseWikilinks, resolveWikilink } from "$lib/wikilink";
9
+
import type { Heading, WikiLink } from "$lib/wikilink";
16
10
import { Tag } from "$ui/Tag";
17
11
import rehypeShiki from "@shikijs/rehype";
18
12
import { A, useParams } from "@solidjs/router";
···
118
112
<A href={`/notes/edit/${n().id}`}>
119
113
<Button variant="secondary">Edit</Button>
120
114
</A>
115
+
<Button variant="secondary" onClick={() => api.downloadNoteAsMarkdown(n())}>Download Markdown</Button>
121
116
</div>
122
117
</header>
123
118