Add EntryCrawler that uses readability lib
Actors delegating to actors baybeeee
This commit is contained in:
parent
f13c7e5e70
commit
b7efc61cfc
289
Cargo.lock
generated
289
Cargo.lock
generated
@ -14,7 +14,7 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.9",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
@ -26,7 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom 0.2.9",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
@ -467,6 +467,7 @@ dependencies = [
|
|||||||
"maud",
|
"maud",
|
||||||
"notify",
|
"notify",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"readability",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
@ -889,6 +890,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futf"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
|
||||||
|
dependencies = [
|
||||||
|
"mac",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.28"
|
version = "0.3.28"
|
||||||
@ -999,6 +1010,17 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@ -1131,6 +1153,20 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html5ever"
|
||||||
|
version = "0.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"mac",
|
||||||
|
"markup5ever",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@ -1499,6 +1535,38 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"phf",
|
||||||
|
"phf_codegen",
|
||||||
|
"string_cache",
|
||||||
|
"string_cache_codegen",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever_rcdom"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
|
||||||
|
dependencies = [
|
||||||
|
"html5ever",
|
||||||
|
"markup5ever",
|
||||||
|
"tendril",
|
||||||
|
"xml5ever",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -1618,7 +1686,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1639,6 +1707,12 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "new_debug_unreachable"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@ -1689,7 +1763,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -1846,6 +1920,63 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.8.0",
|
||||||
|
"phf_shared 0.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.8.0",
|
||||||
|
"rand 0.7.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@ -1924,6 +2055,12 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "precomputed-hash"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@ -1985,6 +2122,20 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.1.16",
|
||||||
|
"libc",
|
||||||
|
"rand_chacha 0.2.2",
|
||||||
|
"rand_core 0.5.1",
|
||||||
|
"rand_hc",
|
||||||
|
"rand_pcg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -1992,8 +2143,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2003,7 +2164,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.1.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2012,7 +2182,25 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_hc"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_pcg"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2037,6 +2225,20 @@ dependencies = [
|
|||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "readability"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7843b159286299dd2b90f06d904ae1a8017a650d88d716c85dd6f123947f399"
|
||||||
|
dependencies = [
|
||||||
|
"html5ever",
|
||||||
|
"lazy_static",
|
||||||
|
"markup5ever_rcdom",
|
||||||
|
"regex",
|
||||||
|
"reqwest",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -2142,7 +2344,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"signature",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -2389,7 +2591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
|
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2590,7 +2792,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
@ -2631,7 +2833,7 @@ dependencies = [
|
|||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
@ -2669,6 +2871,32 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
|
||||||
|
dependencies = [
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
"precomputed-hash",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache_codegen"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.10.0",
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -2732,6 +2960,17 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tendril"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
|
||||||
|
dependencies = [
|
||||||
|
"futf",
|
||||||
|
"mac",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -3113,6 +3352,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -3125,7 +3370,7 @@ version = "1.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
|
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.9",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3209,6 +3454,12 @@ dependencies = [
|
|||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.0+wasi-snapshot-preview1"
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
@ -3518,6 +3769,18 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml5ever"
|
||||||
|
version = "0.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"mac",
|
||||||
|
"markup5ever",
|
||||||
|
"time 0.1.45",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -24,6 +24,7 @@ feed-rs = "1.3"
|
|||||||
maud = { version = "0.25", features = ["axum"] }
|
maud = { version = "0.25", features = ["axum"] }
|
||||||
notify = "6"
|
notify = "6"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
|
readability = "0.2"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_with = "3"
|
serde_with = "3"
|
||||||
|
180
src/actors/entry_crawler.rs
Normal file
180
src/actors/entry_crawler.rs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bytes::Buf;
|
||||||
|
use feed_rs::parser;
|
||||||
|
use readability::extractor;
|
||||||
|
use reqwest::Client;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use tokio::sync::{broadcast, mpsc, Mutex};
|
||||||
|
use tracing::{info, instrument};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::models::entry::{update_entry, CreateEntry, Entry};
|
||||||
|
use crate::models::feed::{upsert_feed, CreateFeed, Feed};
|
||||||
|
|
||||||
|
/// The `EntryCrawler` actor fetches an entry url, extracts the content, and saves the content to
|
||||||
|
/// the file system and any associated metadata to the database.
|
||||||
|
///
|
||||||
|
/// It receives `EntryCrawlerMessage` messages via the `receiver` channel. It communicates back to
|
||||||
|
/// the sender of those messages via the `respond_to` channel on the `EntryCrawlerMessage`.
|
||||||
|
///
|
||||||
|
/// `EntryCrawler` should not be instantiated directly. Instead, use the `EntryCrawlerHandle`.
|
||||||
|
struct EntryCrawler {
|
||||||
|
receiver: mpsc::Receiver<EntryCrawlerMessage>,
|
||||||
|
pool: PgPool,
|
||||||
|
client: Client,
|
||||||
|
content_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum EntryCrawlerMessage {
|
||||||
|
Crawl {
|
||||||
|
entry: Entry,
|
||||||
|
respond_to: broadcast::Sender<EntryCrawlerHandleMessage>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for EntryCrawlerMessage {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
EntryCrawlerMessage::Crawl { entry, .. } => write!(f, "Crawl({})", entry.url),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error type that enumerates possible failures during a crawl and is cloneable and can be sent
|
||||||
|
/// across threads (does not reference the originating Errors which are usually not cloneable).
|
||||||
|
#[derive(thiserror::Error, Debug, Clone)]
|
||||||
|
pub enum EntryCrawlerError {
|
||||||
|
#[error("invalid entry url: {0}")]
|
||||||
|
InvalidUrl(String),
|
||||||
|
#[error("failed to fetch entry: {0}")]
|
||||||
|
FetchError(String),
|
||||||
|
#[error("failed to extract content for entry: {0}")]
|
||||||
|
ExtractError(String),
|
||||||
|
#[error("failed to create entry: {0}")]
|
||||||
|
CreateEntryError(String),
|
||||||
|
#[error("failed to save entry content: {0}")]
|
||||||
|
SaveContentError(String),
|
||||||
|
}
|
||||||
|
pub type EntryCrawlerResult<T, E = EntryCrawlerError> = ::std::result::Result<T, E>;
|
||||||
|
|
||||||
|
impl EntryCrawler {
|
||||||
|
fn new(
|
||||||
|
receiver: mpsc::Receiver<EntryCrawlerMessage>,
|
||||||
|
pool: PgPool,
|
||||||
|
client: Client,
|
||||||
|
content_dir: String,
|
||||||
|
) -> Self {
|
||||||
|
EntryCrawler {
|
||||||
|
receiver,
|
||||||
|
pool,
|
||||||
|
client,
|
||||||
|
content_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, fields(entry = %entry.url))]
|
||||||
|
async fn crawl_entry(&self, entry: Entry) -> EntryCrawlerResult<Entry> {
|
||||||
|
info!("Fetching and parsing entry");
|
||||||
|
let content_dir = Path::new(&self.content_dir);
|
||||||
|
let url =
|
||||||
|
Url::parse(&entry.url).map_err(|_| EntryCrawlerError::InvalidUrl(entry.url.clone()))?;
|
||||||
|
let bytes = self
|
||||||
|
.client
|
||||||
|
.get(url.clone())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|_| EntryCrawlerError::FetchError(entry.url.clone()))?
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|_| EntryCrawlerError::FetchError(entry.url.clone()))?;
|
||||||
|
let article = extractor::extract(&mut bytes.reader(), &url)
|
||||||
|
.map_err(|_| EntryCrawlerError::ExtractError(entry.url.clone()))?;
|
||||||
|
let id = entry.entry_id;
|
||||||
|
// TODO: update entry with scraped data
|
||||||
|
// if let Some(date) = article.date {
|
||||||
|
// // prefer scraped date over rss feed date
|
||||||
|
// let mut updated_entry = entry.clone();
|
||||||
|
// updated_entry.published_at = date;
|
||||||
|
// entry = update_entry(&self.pool, updated_entry)
|
||||||
|
// .await
|
||||||
|
// .map_err(|_| EntryCrawlerError::CreateEntryError(entry.url.clone()))?;
|
||||||
|
// };
|
||||||
|
fs::write(content_dir.join(format!("{}.html", id)), article.content)
|
||||||
|
.map_err(|_| EntryCrawlerError::SaveContentError(entry.url.clone()))?;
|
||||||
|
fs::write(content_dir.join(format!("{}.txt", id)), article.text)
|
||||||
|
.map_err(|_| EntryCrawlerError::SaveContentError(entry.url.clone()))?;
|
||||||
|
Ok(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, fields(msg = %msg))]
|
||||||
|
async fn handle_message(&mut self, msg: EntryCrawlerMessage) {
|
||||||
|
match msg {
|
||||||
|
EntryCrawlerMessage::Crawl { entry, respond_to } => {
|
||||||
|
let result = self.crawl_entry(entry).await;
|
||||||
|
// ignore the result since the initiator may have cancelled waiting for the
|
||||||
|
// response, and that is ok
|
||||||
|
let _ = respond_to.send(EntryCrawlerHandleMessage::Entry(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn run(&mut self) {
|
||||||
|
info!("starting entry crawler");
|
||||||
|
while let Some(msg) = self.receiver.recv().await {
|
||||||
|
self.handle_message(msg).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `EntryCrawlerHandle` is used to initialize and communicate with a `EntryCrawler` actor.
|
||||||
|
///
|
||||||
|
/// The `EntryCrawler` actor fetches a feed url, parses it, and saves it to the database. It runs
|
||||||
|
/// as a separate asynchronous task from the main web server and communicates via channels.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct EntryCrawlerHandle {
|
||||||
|
sender: mpsc::Sender<EntryCrawlerMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `EntryCrawlerHandleMessage` is the response to a `EntryCrawlerMessage` sent to the
|
||||||
|
/// `EntryCrawlerHandle`.
|
||||||
|
///
|
||||||
|
/// `EntryCrawlerHandleMessage::Entry` contains the result of crawling an entry url.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum EntryCrawlerHandleMessage {
|
||||||
|
Entry(EntryCrawlerResult<Entry>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntryCrawlerHandle {
|
||||||
|
/// Creates an async actor task that will listen for messages on the `sender` channel.
|
||||||
|
pub fn new(pool: PgPool, client: Client, content_dir: String) -> Self {
|
||||||
|
let (sender, receiver) = mpsc::channel(8);
|
||||||
|
let mut crawler = EntryCrawler::new(receiver, pool, client, content_dir);
|
||||||
|
tokio::spawn(async move { crawler.run().await });
|
||||||
|
|
||||||
|
Self { sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a `EntryCrawlerMessage::Crawl` message to the running `EntryCrawler` actor.
|
||||||
|
///
|
||||||
|
/// Listen to the result of the crawl via the returned `broadcast::Receiver`.
|
||||||
|
pub async fn crawl(&self, entry: Entry) -> broadcast::Receiver<EntryCrawlerHandleMessage> {
|
||||||
|
let (sender, receiver) = broadcast::channel(8);
|
||||||
|
let msg = EntryCrawlerMessage::Crawl {
|
||||||
|
entry,
|
||||||
|
respond_to: sender,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.sender
|
||||||
|
.send(msg)
|
||||||
|
.await
|
||||||
|
.expect("entry crawler task has died");
|
||||||
|
receiver
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,21 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use feed_rs::parser;
|
use feed_rs::parser;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::sync::{broadcast, mpsc};
|
||||||
use tracing::{info, instrument};
|
use tracing::log::warn;
|
||||||
|
use tracing::{info, info_span, instrument};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::models::entry::Entry;
|
use crate::actors::entry_crawler::EntryCrawlerHandle;
|
||||||
|
use crate::models::entry::{upsert_entries, CreateEntry, Entry};
|
||||||
use crate::models::feed::{upsert_feed, CreateFeed, Feed};
|
use crate::models::feed::{upsert_feed, CreateFeed, Feed};
|
||||||
|
|
||||||
/// The `FeedCrawler` actor fetches a feed url, parses it, and saves it to the database.
|
/// The `FeedCrawler` actor fetches a feed url, parses it, and saves it to the database.
|
||||||
///
|
///
|
||||||
/// It receives `FeedCrawlerMessage` messages via the `receiver` channel. It communicates back to
|
/// It receives `FeedCrawlerMessage` messages via the `receiver` channel. It communicates back to
|
||||||
/// the sender of those messages via the `respond_to` channel on the `FeedCrawlerMessage`.
|
/// the sender of those messages via the `respond_to` channel on the `FeedCrawlerMessage`.
|
||||||
///
|
///
|
||||||
/// `FeedCrawler` should not be instantiated directly. Instead, use the `FeedCrawlerHandle`.
|
/// `FeedCrawler` should not be instantiated directly. Instead, use the `FeedCrawlerHandle`.
|
||||||
@ -20,6 +23,7 @@ struct FeedCrawler {
|
|||||||
receiver: mpsc::Receiver<FeedCrawlerMessage>,
|
receiver: mpsc::Receiver<FeedCrawlerMessage>,
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
content_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -38,7 +42,7 @@ impl Display for FeedCrawlerMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error type that enumerates possible failures during a crawl and is cloneable and can be sent
|
/// An error type that enumerates possible failures during a crawl and is cloneable and can be sent
|
||||||
/// across threads (does not reference the originating Errors which are usually not cloneable).
|
/// across threads (does not reference the originating Errors which are usually not cloneable).
|
||||||
#[derive(thiserror::Error, Debug, Clone)]
|
#[derive(thiserror::Error, Debug, Clone)]
|
||||||
pub enum FeedCrawlerError {
|
pub enum FeedCrawlerError {
|
||||||
@ -48,15 +52,23 @@ pub enum FeedCrawlerError {
|
|||||||
ParseError(Url),
|
ParseError(Url),
|
||||||
#[error("failed to create feed: {0}")]
|
#[error("failed to create feed: {0}")]
|
||||||
CreateFeedError(Url),
|
CreateFeedError(Url),
|
||||||
|
#[error("failed to create feed entries: {0}")]
|
||||||
|
CreateFeedEntriesError(Url),
|
||||||
}
|
}
|
||||||
pub type FeedCrawlerResult<T, E = FeedCrawlerError> = ::std::result::Result<T, E>;
|
pub type FeedCrawlerResult<T, E = FeedCrawlerError> = ::std::result::Result<T, E>;
|
||||||
|
|
||||||
impl FeedCrawler {
|
impl FeedCrawler {
|
||||||
fn new(receiver: mpsc::Receiver<FeedCrawlerMessage>, pool: PgPool, client: Client) -> Self {
|
fn new(
|
||||||
|
receiver: mpsc::Receiver<FeedCrawlerMessage>,
|
||||||
|
pool: PgPool,
|
||||||
|
client: Client,
|
||||||
|
content_dir: String,
|
||||||
|
) -> Self {
|
||||||
FeedCrawler {
|
FeedCrawler {
|
||||||
receiver,
|
receiver,
|
||||||
pool,
|
pool,
|
||||||
client,
|
client,
|
||||||
|
content_dir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +99,40 @@ impl FeedCrawler {
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| FeedCrawlerError::CreateFeedError(url.clone()))?;
|
.map_err(|_| FeedCrawlerError::CreateFeedError(url.clone()))?;
|
||||||
info!(%feed.feed_id, "upserted feed");
|
info!(%feed.feed_id, "upserted feed");
|
||||||
|
|
||||||
|
let mut payload = Vec::with_capacity(parsed_feed.entries.len());
|
||||||
|
for entry in parsed_feed.entries {
|
||||||
|
let entry_span = info_span!("entry", id = entry.id);
|
||||||
|
let _entry_span_guard = entry_span.enter();
|
||||||
|
if let Some(link) = entry.links.get(0) {
|
||||||
|
// if no scraped or feed date is available, fallback to the current time
|
||||||
|
let published_at = entry.published.unwrap_or_else(Utc::now);
|
||||||
|
let entry = CreateEntry {
|
||||||
|
title: entry.title.map(|t| t.content),
|
||||||
|
url: link.href.clone(),
|
||||||
|
description: entry.summary.map(|s| s.content),
|
||||||
|
feed_id: feed.feed_id,
|
||||||
|
published_at,
|
||||||
|
};
|
||||||
|
payload.push(entry);
|
||||||
|
} else {
|
||||||
|
warn!("Skipping feed entry with no links");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let entries = upsert_entries(&self.pool, payload)
|
||||||
|
.await
|
||||||
|
.map_err(|_| FeedCrawlerError::CreateFeedEntriesError(url.clone()))?;
|
||||||
|
info!("Created {} entries", entries.len());
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry_crawler = EntryCrawlerHandle::new(
|
||||||
|
self.pool.clone(),
|
||||||
|
self.client.clone(),
|
||||||
|
self.content_dir.clone(),
|
||||||
|
);
|
||||||
|
// TODO: ignoring this receiver for the time being, pipe through events eventually
|
||||||
|
let _ = entry_crawler.crawl(entry).await;
|
||||||
|
}
|
||||||
Ok(feed)
|
Ok(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +170,7 @@ pub struct FeedCrawlerHandle {
|
|||||||
/// `FeedCrawlerHandle`.
|
/// `FeedCrawlerHandle`.
|
||||||
///
|
///
|
||||||
/// `FeedCrawlerHandleMessage::Feed` contains the result of crawling a feed url.
|
/// `FeedCrawlerHandleMessage::Feed` contains the result of crawling a feed url.
|
||||||
/// `FeedCrawlerHandleMessage::Entry` contains the result of crawling an entry url.
|
/// `FeedCrawlerHandleMessage::Entry` contains the result of crawling an entry url within the feed.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum FeedCrawlerHandleMessage {
|
pub enum FeedCrawlerHandleMessage {
|
||||||
Feed(FeedCrawlerResult<Feed>),
|
Feed(FeedCrawlerResult<Feed>),
|
||||||
@ -133,9 +179,9 @@ pub enum FeedCrawlerHandleMessage {
|
|||||||
|
|
||||||
impl FeedCrawlerHandle {
|
impl FeedCrawlerHandle {
|
||||||
/// Creates an async actor task that will listen for messages on the `sender` channel.
|
/// Creates an async actor task that will listen for messages on the `sender` channel.
|
||||||
pub fn new(pool: PgPool, client: Client) -> Self {
|
pub fn new(pool: PgPool, client: Client, content_dir: String) -> Self {
|
||||||
let (sender, receiver) = mpsc::channel(8);
|
let (sender, receiver) = mpsc::channel(8);
|
||||||
let mut crawler = FeedCrawler::new(receiver, pool, client);
|
let mut crawler = FeedCrawler::new(receiver, pool, client, content_dir);
|
||||||
tokio::spawn(async move { crawler.run().await });
|
tokio::spawn(async move { crawler.run().await });
|
||||||
|
|
||||||
Self { sender }
|
Self { sender }
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
pub mod entry_crawler;
|
||||||
pub mod feed_crawler;
|
pub mod feed_crawler;
|
||||||
|
@ -17,6 +17,7 @@ use tokio_stream::StreamExt;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::actors::feed_crawler::{FeedCrawlerHandle, FeedCrawlerHandleMessage};
|
use crate::actors::feed_crawler::{FeedCrawlerHandle, FeedCrawlerHandleMessage};
|
||||||
|
use crate::config::Config;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::models::entry::get_entries_for_feed;
|
use crate::models::entry::get_entries_for_feed;
|
||||||
use crate::models::feed::{create_feed, delete_feed, get_feed, CreateFeed, FeedType};
|
use crate::models::feed::{create_feed, delete_feed, get_feed, CreateFeed, FeedType};
|
||||||
@ -108,11 +109,13 @@ impl IntoResponse for AddFeedError {
|
|||||||
pub async fn post(
|
pub async fn post(
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
State(crawls): State<Crawls>,
|
State(crawls): State<Crawls>,
|
||||||
|
State(config): State<Config>,
|
||||||
Form(add_feed): Form<AddFeed>,
|
Form(add_feed): Form<AddFeed>,
|
||||||
) -> AddFeedResult<Response> {
|
) -> AddFeedResult<Response> {
|
||||||
// TODO: store the client in axum state (as long as it can be used concurrently?)
|
// TODO: store the client in axum state (as long as it can be used concurrently?)
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let feed_crawler = FeedCrawlerHandle::new(pool.clone(), client.clone());
|
let feed_crawler =
|
||||||
|
FeedCrawlerHandle::new(pool.clone(), client.clone(), config.content_dir.clone());
|
||||||
|
|
||||||
let feed = create_feed(
|
let feed = create_feed(
|
||||||
&pool,
|
&pool,
|
||||||
|
@ -13,6 +13,7 @@ use crate::models::feed::get_feeds;
|
|||||||
use crate::models::entry::{update_entry, upsert_entries, CreateEntry};
|
use crate::models::entry::{update_entry, upsert_entries, CreateEntry};
|
||||||
use crate::uuid::Base62Uuid;
|
use crate::uuid::Base62Uuid;
|
||||||
|
|
||||||
|
/// DEPRECATED: Use FeedCrawler instead, keeping this for reference until I set up scheduled jobs.
|
||||||
/// For every feed in the database, fetches the feed, parses it, and saves new entries to the
|
/// For every feed in the database, fetches the feed, parses it, and saves new entries to the
|
||||||
/// database.
|
/// database.
|
||||||
pub async fn crawl(pool: &PgPool) -> anyhow::Result<()> {
|
pub async fn crawl(pool: &PgPool) -> anyhow::Result<()> {
|
||||||
|
@ -146,6 +146,37 @@ pub async fn create_entry(pool: &PgPool, payload: CreateEntry) -> Result<Entry>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn upsert_entry(pool: &PgPool, payload: CreateEntry) -> Result<Entry> {
|
||||||
|
payload.validate()?;
|
||||||
|
sqlx::query_as!(
|
||||||
|
Entry,
|
||||||
|
"insert into entry (
|
||||||
|
title, url, description, feed_id, published_at
|
||||||
|
) values (
|
||||||
|
$1, $2, $3, $4, $5
|
||||||
|
) on conflict (url, feed_id) do update set
|
||||||
|
title = excluded.title,
|
||||||
|
description = excluded.description,
|
||||||
|
published_at = excluded.published_at
|
||||||
|
returning *",
|
||||||
|
payload.title,
|
||||||
|
payload.url,
|
||||||
|
payload.description,
|
||||||
|
payload.feed_id,
|
||||||
|
payload.published_at,
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
if let sqlx::error::Error::Database(ref psql_error) = error {
|
||||||
|
if psql_error.code().as_deref() == Some("23503") {
|
||||||
|
return Error::RelationNotFound("feed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Error::Sqlx(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_entries(pool: &PgPool, payload: Vec<CreateEntry>) -> Result<Vec<Entry>> {
|
pub async fn create_entries(pool: &PgPool, payload: Vec<CreateEntry>) -> Result<Vec<Entry>> {
|
||||||
let mut titles = Vec::with_capacity(payload.len());
|
let mut titles = Vec::with_capacity(payload.len());
|
||||||
let mut urls = Vec::with_capacity(payload.len());
|
let mut urls = Vec::with_capacity(payload.len());
|
||||||
@ -209,7 +240,10 @@ pub async fn upsert_entries(pool: &PgPool, payload: Vec<CreateEntry>) -> Result<
|
|||||||
"insert into entry (
|
"insert into entry (
|
||||||
title, url, description, feed_id, published_at
|
title, url, description, feed_id, published_at
|
||||||
) select * from unnest($1::text[], $2::text[], $3::text[], $4::uuid[], $5::timestamptz[])
|
) select * from unnest($1::text[], $2::text[], $3::text[], $4::uuid[], $5::timestamptz[])
|
||||||
on conflict do nothing
|
on conflict (url, feed_id) do update set
|
||||||
|
title = excluded.title,
|
||||||
|
description = excluded.description,
|
||||||
|
published_at = excluded.published_at
|
||||||
returning *",
|
returning *",
|
||||||
titles.as_slice() as &[Option<String>],
|
titles.as_slice() as &[Option<String>],
|
||||||
urls.as_slice(),
|
urls.as_slice(),
|
||||||
|
@ -94,6 +94,7 @@ impl Layout {
|
|||||||
head {
|
head {
|
||||||
meta charset="utf-8";
|
meta charset="utf-8";
|
||||||
title { (self.title) }
|
title { (self.title) }
|
||||||
|
// TODO: vendor this before going to prod
|
||||||
script type="module" {
|
script type="module" {
|
||||||
r#"import * as Turbo from 'https://cdn.skypack.dev/@hotwired/turbo';"#
|
r#"import * as Turbo from 'https://cdn.skypack.dev/@hotwired/turbo';"#
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user