Compare commits
8 commits
413dc6ab9a
...
86aede7484
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86aede7484 | ||
|
|
081cfa1a86 | ||
|
|
8cbb2ef0c5 | ||
|
|
45a56b9fe9 | ||
|
|
18c8749f91 | ||
|
|
71e1086ff7 | ||
|
|
bb33e28849 | ||
|
|
b3cdf9f8ef |
17 changed files with 518 additions and 475 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,3 +2,5 @@
|
||||||
blogdor.db
|
blogdor.db
|
||||||
secrets
|
secrets
|
||||||
.env
|
.env
|
||||||
|
.#*
|
||||||
|
dump.sql
|
||||||
|
|
|
||||||
220
Cargo.lock
generated
220
Cargo.lock
generated
|
|
@ -112,9 +112,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.7"
|
version = "0.8.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
|
|
@ -143,9 +143,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.5.5"
|
version = "0.5.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -167,7 +167,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -226,9 +226,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.19.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
|
@ -244,9 +244,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.49"
|
version = "1.2.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
|
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
|
@ -275,7 +275,7 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -309,7 +309,7 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -434,7 +434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -468,7 +468,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -479,9 +479,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.10"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
|
checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa-short"
|
name = "dtoa-short"
|
||||||
|
|
@ -574,9 +574,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
|
|
@ -849,7 +849,7 @@ dependencies = [
|
||||||
"markup5ever 0.12.1",
|
"markup5ever 0.12.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -910,9 +910,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
|
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -965,9 +965,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.17"
|
version = "0.1.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1061,9 +1061,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.1.1"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
|
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
|
|
@ -1075,9 +1075,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.1.1"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
|
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
|
|
@ -1133,9 +1133,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.9"
|
version = "0.7.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1149,9 +1149,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
|
|
@ -1217,13 +1217,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.10"
|
version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1321,7 +1321,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1494,7 +1494,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1539,9 +1539,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1599,7 +1599,7 @@ dependencies = [
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1676,9 +1676,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.103"
|
version = "1.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
@ -1776,6 +1776,15 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.2"
|
||||||
|
|
@ -1807,9 +1816,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.24"
|
version = "0.12.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1881,9 +1890,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.2"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
|
|
@ -1907,9 +1916,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.13.1"
|
version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
|
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -1933,9 +1942,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.20"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
|
|
@ -2011,20 +2020,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.145"
|
version = "1.0.148"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2089,10 +2098,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.7"
|
version = "1.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2214,7 +2224,7 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros-core",
|
"sqlx-macros-core",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2237,7 +2247,7 @@ dependencies = [
|
||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
@ -2415,9 +2425,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.111"
|
version = "2.0.112"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2441,7 +2451,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2467,9 +2477,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.23.0"
|
version = "3.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
|
|
@ -2515,7 +2525,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2526,7 +2536,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2588,7 +2598,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2652,9 +2662,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.7"
|
version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -2682,9 +2692,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.43"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
|
@ -2700,14 +2710,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.35"
|
version = "0.1.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"valuable",
|
"valuable",
|
||||||
|
|
@ -2937,7 +2947,7 @@ dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2999,9 +3009,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
"windows-interface",
|
"windows-interface",
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
"windows-result 0.4.1",
|
"windows-result",
|
||||||
"windows-strings 0.5.1",
|
"windows-strings",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3012,7 +3022,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3023,15 +3033,9 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-link"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -3040,22 +3044,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.5.3"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.1.3",
|
"windows-link",
|
||||||
"windows-result 0.3.4",
|
"windows-result",
|
||||||
"windows-strings 0.4.2",
|
"windows-strings",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-result"
|
|
||||||
version = "0.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link 0.1.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3064,16 +3059,7 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-strings"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link 0.1.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3082,7 +3068,7 @@ version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3118,7 +3104,7 @@ version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3158,7 +3144,7 @@ version = "0.53.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.1",
|
"windows-link",
|
||||||
"windows_aarch64_gnullvm 0.53.1",
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
"windows_aarch64_msvc 0.53.1",
|
"windows_aarch64_msvc 0.53.1",
|
||||||
"windows_i686_gnu 0.53.1",
|
"windows_i686_gnu 0.53.1",
|
||||||
|
|
@ -3358,7 +3344,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3379,7 +3365,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3399,7 +3385,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3439,5 +3425,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.112",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd"
|
||||||
|
|
|
||||||
28
Cargo.toml
28
Cargo.toml
|
|
@ -5,23 +5,23 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8.7", default-features = false, features = ["http1", "http2", "json", "macros", "tokio"] }
|
axum = { version = "0.8", default-features = false, features = ["http1", "http2", "json", "macros", "tokio"] }
|
||||||
clap = { version = "4.5.53", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
feed-rs = { version = "2.3.1", features = ["sanitize"] }
|
feed-rs = { version = "2.3", features = ["sanitize"] }
|
||||||
html2md = "0.2.15"
|
html2md = "0.2"
|
||||||
justerror = "1.1.0"
|
justerror = "1"
|
||||||
reqwest = "0.12.24"
|
reqwest = "0.12"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
sqlx = { version = "0.8.6", default-features = false, features = ["chrono", "derive", "macros", "migrate", "runtime-tokio", "sqlite", "tls-none"] }
|
sqlx = { version = "0.8", default-features = false, features = ["chrono", "derive", "macros", "migrate", "runtime-tokio", "sqlite", "tls-none"] }
|
||||||
thiserror = "2.0.17"
|
thiserror = "2"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48", features = ["full"] }
|
||||||
tokio-util = "0.7.17"
|
tokio-util = "0.7"
|
||||||
tracing = "0.1.43"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12"
|
||||||
winnow = "0.7.14"
|
winnow = "0.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS feeds;
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS feeds (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
url TEXT UNIQUE NOT NULL,
|
|
||||||
added_by INT NOT NULL,
|
|
||||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT current_timestamp,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT current_timestamp,
|
|
||||||
FOREIGN KEY (added_by) REFERENCES users(zulip_id)
|
|
||||||
);
|
|
||||||
2
migrations/0002_feeds_and_status.down.sql
Normal file
2
migrations/0002_feeds_and_status.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP TABLE IF EXISTS status;
|
||||||
|
DROP TABLE IF EXISTS feeds;
|
||||||
16
migrations/0002_feeds_and_status.up.sql
Normal file
16
migrations/0002_feeds_and_status.up.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS feeds (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
owner INT NOT NULL,
|
||||||
|
created DATETIME NOT NULL DEFAULT current_timestamp,
|
||||||
|
FOREIGN KEY (owner) REFERENCES users(zulip_id),
|
||||||
|
UNIQUE(url, owner)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS status (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
feed INTEGER NOT NULL,
|
||||||
|
updated DATETIME NOT NULL DEFAULT current_timestamp,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (feed) REFERENCES feeds(id)
|
||||||
|
);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
CREATE TABLE IF NOT EXISTS successful_runs (
|
CREATE TABLE IF NOT EXISTS runs (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
date_time DATETIME NOT NULL DEFAULT current_timestamp,
|
|
||||||
feed INTEGER NOT NULL,
|
feed INTEGER NOT NULL,
|
||||||
|
fetched DATETIME NOT NULL DEFAULT current_timestamp,
|
||||||
|
posted DATETIME, -- once this becomes non-null, the program will ensure it will never be null again
|
||||||
FOREIGN KEY (feed) REFERENCES feeds(id)
|
FOREIGN KEY (feed) REFERENCES feeds(id)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS fetches;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS fetches (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
feed INT NOT NULL,
|
|
||||||
fetched DATETIME NOT NULL DEFAULT current_timestamp,
|
|
||||||
FOREIGN KEY (feed) REFERENCES feeds(id)
|
|
||||||
);
|
|
||||||
10
sample-data/invalid-url.json
Normal file
10
sample-data/invalid-url.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"bot_email": "blogdor-outgoing-bot@zulip-host",
|
||||||
|
"token": "another_token",
|
||||||
|
"message": {
|
||||||
|
"sender_email": "sender-email",
|
||||||
|
"sender_id": 1,
|
||||||
|
"sender_full_name": "magoo",
|
||||||
|
"content": "@**blogdor's manager** add invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
sample-data/list.json
Normal file
10
sample-data/list.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"bot_email": "blogdor-outgoing-bot@zulip-host",
|
||||||
|
"token": "another_token",
|
||||||
|
"message": {
|
||||||
|
"sender_email": "sender-email",
|
||||||
|
"sender_id": 1,
|
||||||
|
"sender_full_name": "magoo",
|
||||||
|
"content": "@**blogdor's manager** list"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/db.rs
45
src/db.rs
|
|
@ -5,57 +5,16 @@ const TIMEOUT: u64 = 2000; // in milliseconds
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
Sqlite, SqlitePool,
|
SqlitePool,
|
||||||
query::Query,
|
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
|
||||||
sqlite::{
|
|
||||||
SqliteArguments, SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteRow,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::BlogdorTheAggregator;
|
use crate::BlogdorTheAggregator;
|
||||||
|
|
||||||
pub enum DbAction<'q> {
|
|
||||||
Execute(Query<'q, Sqlite, SqliteArguments<'q>>),
|
|
||||||
FetchOne(Query<'q, Sqlite, SqliteArguments<'q>>),
|
|
||||||
FetchMany(Query<'q, Sqlite, SqliteArguments<'q>>),
|
|
||||||
FetchOptional(Query<'q, Sqlite, SqliteArguments<'q>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum DbValue {
|
|
||||||
None,
|
|
||||||
Optional(Option<SqliteRow>),
|
|
||||||
One(SqliteRow),
|
|
||||||
Many(Vec<SqliteRow>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlogdorTheAggregator {
|
impl BlogdorTheAggregator {
|
||||||
pub async fn close_db(&self) {
|
pub async fn close_db(&self) {
|
||||||
self.db.close().await;
|
self.db.close().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn db_action<'q, T>(&self, query: DbAction<'q>) -> Result<DbValue, String> {
|
|
||||||
match query {
|
|
||||||
DbAction::Execute(q) => {
|
|
||||||
q.execute(&self.db).await.map_err(|e| format!("{e}"))?;
|
|
||||||
Ok(DbValue::None)
|
|
||||||
}
|
|
||||||
DbAction::FetchOne(q) => {
|
|
||||||
let r = q.fetch_one(&self.db).await.map_err(|e| format!("{e}"))?;
|
|
||||||
Ok(DbValue::One(r))
|
|
||||||
}
|
|
||||||
DbAction::FetchMany(q) => {
|
|
||||||
let r = q.fetch_all(&self.db).await.map_err(|e| format!("{e}"))?;
|
|
||||||
Ok(DbValue::Many(r))
|
|
||||||
}
|
|
||||||
DbAction::FetchOptional(q) => {
|
|
||||||
let r = q
|
|
||||||
.fetch_optional(&self.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("{e}"))?;
|
|
||||||
Ok(DbValue::Optional(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_db_pool() -> SqlitePool {
|
pub async fn get_db_pool() -> SqlitePool {
|
||||||
|
|
|
||||||
412
src/lib.rs
412
src/lib.rs
|
|
@ -2,9 +2,10 @@ use std::time::Duration;
|
||||||
|
|
||||||
use feed_rs::parser::parse;
|
use feed_rs::parser::parse;
|
||||||
use reqwest::{Client, Response, StatusCode};
|
use reqwest::{Client, Response, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use server::ServerState;
|
use server::ServerState;
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
SqlitePool,
|
FromRow, SqlitePool,
|
||||||
types::chrono::{DateTime, Utc},
|
types::chrono::{DateTime, Utc},
|
||||||
};
|
};
|
||||||
use tokio::{sync::mpsc::UnboundedSender, task::JoinSet};
|
use tokio::{sync::mpsc::UnboundedSender, task::JoinSet};
|
||||||
|
|
@ -12,7 +13,6 @@ use tokio_util::{bytes::Buf, sync::CancellationToken};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
use db::DbAction;
|
|
||||||
|
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
|
@ -23,9 +23,13 @@ const LAST_FETCHED: DateTime<Utc> = DateTime::from_timestamp_nanos(0);
|
||||||
|
|
||||||
const STALE_FETCH_THRESHOLD: Duration = Duration::from_hours(24);
|
const STALE_FETCH_THRESHOLD: Duration = Duration::from_hours(24);
|
||||||
|
|
||||||
const ADD_FEED_QUERY: &str = "";
|
const ACTIVE_FEEDS_QUERY: &str = r#"SELECT id, url, owner, created, updated, fetched, posted, s.feed feed FROM feeds
|
||||||
const ACTIVE_FEEDS_QUERY: &str = "select id, url from feeds where active = true";
|
INNER JOIN
|
||||||
const STALE_FEEDS_QUERY: &str = "select id, url, added_by, created_at from feeds";
|
(SELECT feed, updated FROM (SELECT feed, MAX(id), updated, active FROM status GROUP BY feed) WHERE active = TRUE) s
|
||||||
|
ON feeds.id = s.feed
|
||||||
|
LEFT JOIN
|
||||||
|
(SELECT posted, MAX(fetched) fetched, feed FROM runs GROUP BY feed) r
|
||||||
|
ON r.feed = feeds.id"#;
|
||||||
|
|
||||||
pub struct BlogdorTheAggregator {
|
pub struct BlogdorTheAggregator {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
|
|
@ -44,6 +48,7 @@ pub struct FeedEntry {
|
||||||
post_url: String,
|
post_url: String,
|
||||||
feed_url: String,
|
feed_url: String,
|
||||||
feed_id: i64,
|
feed_id: i64,
|
||||||
|
owner: i64,
|
||||||
title: String,
|
title: String,
|
||||||
published: DateTime<Utc>,
|
published: DateTime<Utc>,
|
||||||
received: DateTime<Utc>,
|
received: DateTime<Utc>,
|
||||||
|
|
@ -51,17 +56,39 @@ pub struct FeedEntry {
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct FeedResult {
|
pub struct FeedRunResult {
|
||||||
pub entries: Option<Vec<FeedEntry>>,
|
pub entries: Vec<FeedEntry>,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub feed_id: i64,
|
pub feed_id: i64,
|
||||||
|
pub fetched: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct NewFeed {
|
pub struct FeedCommand {
|
||||||
feed: String,
|
feed: String,
|
||||||
user: String,
|
action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
Add,
|
||||||
|
Remove,
|
||||||
|
Help,
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UserRequest {
|
||||||
|
command: FeedCommand,
|
||||||
|
owner: u32,
|
||||||
|
result_sender: UnboundedSender<Result<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for UserRequest {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.command == other.command && self.owner == other.owner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
|
||||||
|
|
@ -82,6 +109,24 @@ enum MessageType {
|
||||||
Direct,
|
Direct,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct ActiveFeed {
|
||||||
|
url: String,
|
||||||
|
id: i64,
|
||||||
|
owner: i64,
|
||||||
|
created: DateTime<Utc>,
|
||||||
|
updated: DateTime<Utc>,
|
||||||
|
#[sqlx(flatten)]
|
||||||
|
last_run: FeedRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct FeedRun {
|
||||||
|
feed: i64,
|
||||||
|
fetched: Option<DateTime<Utc>>,
|
||||||
|
posted: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl BlogdorTheAggregator {
|
impl BlogdorTheAggregator {
|
||||||
pub async fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
let db = db::get_db_pool().await;
|
let db = db::get_db_pool().await;
|
||||||
|
|
@ -120,32 +165,29 @@ impl BlogdorTheAggregator {
|
||||||
self.cancel.cancelled().await
|
self.cancel.cancelled().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn spawn_http(&self, announce_tx: UnboundedSender<NewFeed>, client: reqwest::Client) {
|
pub async fn spawn_http(&self, user_request_tx: UnboundedSender<UserRequest>) {
|
||||||
let state = ServerState::new(
|
let state = ServerState::new(
|
||||||
self.db.clone(),
|
|
||||||
&self.zulip_to_blogdor_email,
|
&self.zulip_to_blogdor_email,
|
||||||
&self.blogdor_token,
|
&self.blogdor_token,
|
||||||
announce_tx,
|
user_request_tx,
|
||||||
client,
|
|
||||||
);
|
);
|
||||||
server::spawn_server(state, self.cancel.clone()).await;
|
server::spawn_server(state, self.cancel.clone()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_feeds(&self) -> Result<Vec<Result<FeedResult, String>>, String> {
|
pub async fn check_feeds(&self) -> Result<Vec<FeedRunResult>, String> {
|
||||||
tracing::debug!("checking feeds");
|
tracing::debug!("checking feeds");
|
||||||
let feeds = sqlx::query!("select id, url from feeds where active = true")
|
let feeds = self
|
||||||
.fetch_all(&self.db)
|
.active_feeds()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e}"))?;
|
.map_err(|_| "could not check feeds".to_string())?;
|
||||||
|
|
||||||
let mut handles = JoinSet::new();
|
let mut handles = JoinSet::new();
|
||||||
for feed in feeds {
|
for feed in feeds {
|
||||||
handles.spawn(check_feed(
|
let id = feed.id;
|
||||||
self.db.clone(),
|
let url = feed.url;
|
||||||
feed.id,
|
let last_run = feed.last_run;
|
||||||
self.client.clone(),
|
let last = last_run.posted.unwrap_or(LAST_FETCHED);
|
||||||
feed.url,
|
handles.spawn(check_feed(self.client.clone(), id, url, last, feed.owner));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut feed_results = Vec::new();
|
let mut feed_results = Vec::new();
|
||||||
|
|
@ -155,6 +197,12 @@ impl BlogdorTheAggregator {
|
||||||
tracing::error!("got join error: {e}");
|
tracing::error!("got join error: {e}");
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let Ok(feed_result) = feed_result else {
|
||||||
|
let e = feed_result.unwrap_err();
|
||||||
|
tracing::error!("got error fetching feed: {e}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
feed_results.push(feed_result);
|
feed_results.push(feed_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,12 +210,9 @@ impl BlogdorTheAggregator {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_stale(&self) {
|
pub async fn check_stale(&self) {
|
||||||
let feeds = match sqlx::query!("select id, url, added_by, created_at from feeds")
|
let feeds = match self.active_feeds().await {
|
||||||
.fetch_all(&self.db)
|
Err(_) => {
|
||||||
.await
|
tracing::error!("could not check stale feeds");
|
||||||
{
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("could not fetch feeds: {e}");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
|
|
@ -176,27 +221,11 @@ impl BlogdorTheAggregator {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
for feed in feeds.into_iter() {
|
for feed in feeds.into_iter() {
|
||||||
let id = feed.id;
|
|
||||||
let url = &feed.url;
|
let url = &feed.url;
|
||||||
let user = feed.added_by;
|
let user = feed.owner;
|
||||||
let fetched = match sqlx::query!(
|
let run = feed.last_run;
|
||||||
"select fetched from fetches where feed = ? order by id desc limit 1",
|
let last = run.fetched.unwrap_or(feed.updated);
|
||||||
id
|
let dur = now - last;
|
||||||
)
|
|
||||||
.fetch_optional(&self.db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("could not get last fetched for {url} from db: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Ok(f) => f,
|
|
||||||
};
|
|
||||||
let dur = if let Some(fetched) = fetched {
|
|
||||||
now - fetched.fetched.and_utc()
|
|
||||||
} else {
|
|
||||||
now - feed.created_at.and_utc()
|
|
||||||
};
|
|
||||||
|
|
||||||
if dur.num_seconds() > STALE_FETCH_THRESHOLD.as_secs() as i64 {
|
if dur.num_seconds() > STALE_FETCH_THRESHOLD.as_secs() as i64 {
|
||||||
let hours = dur.num_hours() % 24;
|
let hours = dur.num_hours() % 24;
|
||||||
|
|
@ -211,7 +240,7 @@ impl BlogdorTheAggregator {
|
||||||
topic: None,
|
topic: None,
|
||||||
};
|
};
|
||||||
if let Err(e) = self.send_zulip_message(&msg).await {
|
if let Err(e) = self.send_zulip_message(&msg).await {
|
||||||
tracing::error!("error sending zulip message to user {user}: {e}");
|
tracing::error!("error sending zulip message to user {user} about {url}: {e}");
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("sent DM to {user} about {url} being fucked");
|
tracing::debug!("sent DM to {user} about {url} being fucked");
|
||||||
}
|
}
|
||||||
|
|
@ -219,15 +248,143 @@ impl BlogdorTheAggregator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn announce_feed(&self, announce: &NewFeed) {
|
pub async fn process_user_request(&self, user_request: &UserRequest) {
|
||||||
let content = format!("{} added a new feed: {}", announce.user, announce.feed);
|
match user_request.command.action {
|
||||||
let msg = ZulipMessage {
|
Action::Add => user_request.result_sender.send(self.add_feed(user_request).await).unwrap_or_default(),
|
||||||
|
Action::Help => user_request.result_sender.send(Ok("DM or `@blogdor's manager` with `add <feed url, RSS or Atom XML files>`, `remove <feed url originally added by you>`, `list` for a list of your feeds, or `help` to get this message (duh).".to_string())).unwrap_or_default(),
|
||||||
|
Action::Remove => user_request.result_sender.send(self.remove_feed(user_request).await).unwrap_or_default(),
|
||||||
|
Action::List => user_request.result_sender.send(self.list_feeds(user_request.owner).await).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_feeds(&self, owner: u32) -> Result<String, String> {
|
||||||
|
let feeds = sqlx::query!("select url, active from feeds left join (select active, feed, max(id) from status group by feed) s on s.feed = feeds.id where feeds.owner = ?", owner).fetch_all(&self.db).await.map_err(|e|{
|
||||||
|
tracing::error!("error getting feeds for {owner}: {e}");
|
||||||
|
"could not get feeds".to_string()
|
||||||
|
})?;
|
||||||
|
let mut active = vec![String::from("Active feeds:")];
|
||||||
|
let mut inactive = vec![String::from("Inactive feeds:")];
|
||||||
|
|
||||||
|
for feed in feeds {
|
||||||
|
let line = format!(" - {}", feed.url);
|
||||||
|
if feed.active.unwrap_or(false) {
|
||||||
|
active.push(line);
|
||||||
|
} else {
|
||||||
|
inactive.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let active = active.join("\n");
|
||||||
|
let inactive = inactive.join("\n");
|
||||||
|
Ok(format!("{active}\n---\n{inactive}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_feed(&self, remove_request: &UserRequest) -> Result<String, String> {
|
||||||
|
let UserRequest { command, owner, .. } = remove_request;
|
||||||
|
let url = &command.feed;
|
||||||
|
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"select id from feeds where url = ? and owner = ?",
|
||||||
|
url,
|
||||||
|
owner
|
||||||
|
)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if matches!(e, sqlx::Error::RowNotFound) {
|
||||||
|
format!("no feed owned by you matches {url}")
|
||||||
|
} else {
|
||||||
|
tracing::error!("got an unknown DB error: {e}");
|
||||||
|
"eek, something went wrong".to_string()
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.id;
|
||||||
|
|
||||||
|
sqlx::query!("insert into status (feed, active) values (?, false)", id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("could not insert deactivate status for feed {id}, got: {e}");
|
||||||
|
"oh no! something went wrong and I couldn't remove your feed".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok("BURNINATED!".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_feed(&self, user_request: &UserRequest) -> Result<String, String> {
|
||||||
|
self.add_user(user_request.owner).await?;
|
||||||
|
|
||||||
|
let url = &user_request.command.feed;
|
||||||
|
let owner = user_request.owner;
|
||||||
|
|
||||||
|
crate::fetch_and_parse_feed(url, &self.client).await?;
|
||||||
|
|
||||||
|
let resp_text;
|
||||||
|
if let Some(id) = sqlx::query!(
|
||||||
|
"select id from feeds where owner = ? and url = ?",
|
||||||
|
owner,
|
||||||
|
url
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("couldn't fetch an optional row from the feeds table: {e}");
|
||||||
|
"whoa, something weird and bad happened".to_string()
|
||||||
|
})?
|
||||||
|
.map(|r| r.id)
|
||||||
|
{
|
||||||
|
sqlx::query!("insert into status (feed, active) values (?, true)", id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("got error inserting into status: {e}");
|
||||||
|
format!(
|
||||||
|
"could not activate previously added feed at {url} for Zulip user {owner}",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
resp_text = format!("marked previously added feed at {url} as active");
|
||||||
|
} else {
|
||||||
|
let txn = self.db.begin().await.map_err(|e| {
|
||||||
|
tracing::error!("got error begining a transaction: {e}");
|
||||||
|
"could not add feed".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"insert into feeds (url, owner) values (?, ?) returning id",
|
||||||
|
url,
|
||||||
|
owner
|
||||||
|
)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("error inserting into feeds: {e}");
|
||||||
|
"could not add feed".to_string()
|
||||||
|
})
|
||||||
|
.map(|i| i.id)?;
|
||||||
|
|
||||||
|
sqlx::query!("insert into status (feed, active) values (?, true)", id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("error inserting into status: {e}");
|
||||||
|
"could not add feed".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// woo!
|
||||||
|
txn.commit().await.map_err(|e| {
|
||||||
|
tracing::error!("error committing add-feed transaction: {e}");
|
||||||
|
"could not add feed".to_string()
|
||||||
|
})?;
|
||||||
|
resp_text = format!("added new feed at {url}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = format!("@**|{}**: added a new feed: {}", owner, url);
|
||||||
|
let zmsg = ZulipMessage {
|
||||||
to: self.channel_id,
|
to: self.channel_id,
|
||||||
typ: MessageType::Stream,
|
typ: MessageType::Stream,
|
||||||
content,
|
content,
|
||||||
topic: Some("New feeds"),
|
topic: Some("New feeds"),
|
||||||
};
|
};
|
||||||
match self.send_zulip_message(&msg).await {
|
match self.send_zulip_message(&zmsg).await {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("got error sending to zulip: {e}");
|
tracing::error!("got error sending to zulip: {e}");
|
||||||
}
|
}
|
||||||
|
|
@ -237,35 +394,41 @@ impl BlogdorTheAggregator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(resp_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// will also update the successful_runs table if it posts to zulip
|
// will also update the runs table with fetched and posted
|
||||||
pub async fn post_entries(&self, posts: &[FeedEntry]) {
|
pub async fn post_entries(&self, feed_run: &FeedRunResult) {
|
||||||
let FeedEntry {
|
let FeedRunResult {
|
||||||
feed_id, received, ..
|
feed_id,
|
||||||
} = posts.last().unwrap();
|
entries,
|
||||||
|
url,
|
||||||
|
..
|
||||||
|
} = feed_run;
|
||||||
|
|
||||||
let mut success = true;
|
let mut success = true;
|
||||||
let Ok(user) = sqlx::query!("select added_by from feeds where id = ?", feed_id)
|
|
||||||
.fetch_one(&self.db)
|
if entries.is_empty() {
|
||||||
.await
|
success = false;
|
||||||
else {
|
tracing::debug!("no new posts from {url}");
|
||||||
tracing::error!("could not get user from db");
|
} else {
|
||||||
return;
|
tracing::debug!("got {} new posts from {url}", entries.len());
|
||||||
};
|
}
|
||||||
let user = user.added_by;
|
|
||||||
for post in posts.iter() {
|
for post in entries.iter() {
|
||||||
|
let owner = post.owner;
|
||||||
let body = post.body.as_deref().unwrap_or("");
|
let body = post.body.as_deref().unwrap_or("");
|
||||||
|
|
||||||
let tail = if body.len() < ZULIP_MESSAGE_CUTOFF {
|
let tail = if body.len() <= ZULIP_MESSAGE_CUTOFF {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
"..."
|
"[...]"
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = post.post_url.as_str();
|
let url = post.post_url.as_str();
|
||||||
let title = post.title.as_str();
|
let title = post.title.as_str();
|
||||||
|
|
||||||
let header = format!("New post in a feed added by @**|{user}**: {title}");
|
let header = format!("New post in a feed added by @**|{owner}**: {title}");
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"{header}\n---\n{body}{tail}\n\n---\noriginally posted to {url}, on {}",
|
"{header}\n---\n{body}{tail}\n\n---\noriginally posted to {url}, on {}",
|
||||||
|
|
@ -295,17 +458,60 @@ impl BlogdorTheAggregator {
|
||||||
}
|
}
|
||||||
tokio::time::sleep(ZULIP_INTERVAL).await;
|
tokio::time::sleep(ZULIP_INTERVAL).await;
|
||||||
}
|
}
|
||||||
if success
|
|
||||||
&& let Err(e) = sqlx::query!(
|
let posted = if success { Some(Utc::now()) } else { None };
|
||||||
"insert into successful_runs (feed, date_time) values (?, ?)",
|
self.record_run(*feed_id, feed_run.fetched, posted).await;
|
||||||
feed_id,
|
}
|
||||||
received
|
|
||||||
)
|
async fn record_run(&self, feed: i64, fetched: DateTime<Utc>, posted: Option<DateTime<Utc>>) {
|
||||||
|
let Ok(db_posted) = sqlx::query!(
|
||||||
|
"select max(posted) posted from runs where feed = ? limit 1",
|
||||||
|
feed
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.db)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
tracing::error!("got db error fetching runs");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let db_posted = db_posted.and_then(|p| p.posted.map(|p| p.and_utc()));
|
||||||
|
let posted = posted.or(db_posted);
|
||||||
|
if let Err(e) = sqlx::query!(
|
||||||
|
"insert into runs (feed, fetched, posted) values (?, ?, ?)",
|
||||||
|
feed,
|
||||||
|
fetched,
|
||||||
|
posted
|
||||||
|
)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("got error adding row to runs: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user(&self, user: u32) -> Result<(), String> {
|
||||||
|
if let Err(e) = sqlx::query!("insert into users (zulip_id) values (?)", user)
|
||||||
.execute(&self.db)
|
.execute(&self.db)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::error!("could not insert run for {feed_id}, got {e}");
|
match e {
|
||||||
|
sqlx::Error::Database(database_error) => {
|
||||||
|
// the users table has only one constraint, which is a uniqueness one on
|
||||||
|
// zulip_id, so if it's violated, we don't care, it just means we already have
|
||||||
|
// that user; if it's not a constraint violation, then something
|
||||||
|
// else and bad has happened
|
||||||
|
if database_error.constraint().is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlx::Error::Io(error) => {
|
||||||
|
tracing::error!("got IO error: {error}");
|
||||||
|
return Err("you should maybe retry that".to_string());
|
||||||
|
}
|
||||||
|
_ => return Err("yikes".to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_zulip_message<'s>(&'s self, msg: &ZulipMessage<'s>) -> Result<Response, String> {
|
async fn send_zulip_message<'s>(&'s self, msg: &ZulipMessage<'s>) -> Result<Response, String> {
|
||||||
|
|
@ -319,6 +525,17 @@ impl BlogdorTheAggregator {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e}"))
|
.map_err(|e| format!("{e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn active_feeds(&self) -> Result<Vec<ActiveFeed>, ()> {
|
||||||
|
let feeds: Vec<ActiveFeed> = sqlx::query_as(ACTIVE_FEEDS_QUERY)
|
||||||
|
.fetch_all(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("error fetching feeds: {e}");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(feeds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait Posted {
|
trait Posted {
|
||||||
|
|
@ -334,33 +551,17 @@ impl Posted for feed_rs::model::Entry {
|
||||||
// takes args by value because it's meant to be called from inside a spawned
|
// takes args by value because it's meant to be called from inside a spawned
|
||||||
// tokio task scope
|
// tokio task scope
|
||||||
async fn check_feed(
|
async fn check_feed(
|
||||||
db: SqlitePool,
|
|
||||||
feed_id: i64,
|
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
feed_id: i64,
|
||||||
url: String,
|
url: String,
|
||||||
) -> Result<FeedResult, String> {
|
last_fetched: DateTime<Utc>,
|
||||||
let rec = sqlx::query!(
|
owner: i64,
|
||||||
"select date_time from successful_runs where feed = ? order by id desc limit 1",
|
) -> Result<FeedRunResult, String> {
|
||||||
feed_id
|
|
||||||
)
|
|
||||||
.fetch_optional(&db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Could not fetch runs for {url} from DB, got {e}"))?;
|
|
||||||
|
|
||||||
tracing::debug!("checking {url}");
|
tracing::debug!("checking {url}");
|
||||||
let last_fetched = rec.map(|d| d.date_time.and_utc()).unwrap_or(LAST_FETCHED);
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
let mut feed = fetch_and_parse_feed(&url, &client).await?;
|
let mut feed = fetch_and_parse_feed(&url, &client).await?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
if let Err(e) = sqlx::query!("insert into fetches (feed) values (?)", feed_id)
|
|
||||||
.execute(&db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!("got error inserting {feed_id} into fetches: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries = None;
|
|
||||||
feed.entries.sort_by_key(|e| std::cmp::Reverse(e.posted()));
|
feed.entries.sort_by_key(|e| std::cmp::Reverse(e.posted()));
|
||||||
for post in feed.entries.into_iter().take(5) {
|
for post in feed.entries.into_iter().take(5) {
|
||||||
if post.posted().unwrap_or(LAST_FETCHED) > last_fetched {
|
if post.posted().unwrap_or(LAST_FETCHED) > last_fetched {
|
||||||
|
|
@ -372,6 +573,7 @@ async fn check_feed(
|
||||||
.map(|l| l.href)
|
.map(|l| l.href)
|
||||||
.unwrap_or("<url not found>".to_string()),
|
.unwrap_or("<url not found>".to_string()),
|
||||||
feed_id,
|
feed_id,
|
||||||
|
owner,
|
||||||
feed_url: url.clone(),
|
feed_url: url.clone(),
|
||||||
title: post
|
title: post
|
||||||
.title
|
.title
|
||||||
|
|
@ -391,14 +593,15 @@ async fn check_feed(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
entries.get_or_insert(Vec::new()).push(entry);
|
entries.push(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(FeedResult {
|
Ok(FeedRunResult {
|
||||||
entries,
|
entries,
|
||||||
url,
|
url,
|
||||||
feed_id,
|
feed_id,
|
||||||
|
fetched: now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,3 +617,6 @@ async fn fetch_and_parse_feed(url: &str, client: &Client) -> Result<feed_rs::mod
|
||||||
|
|
||||||
parse(feed.reader()).map_err(|e| format!("could not parse feed from {url}, got {e}"))
|
parse(feed.reader()).map_err(|e| format!("could not parse feed from {url}, got {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
|
||||||
31
src/main.rs
31
src/main.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use blogdor::{BlogdorTheAggregator, NewFeed};
|
use blogdor::{BlogdorTheAggregator, UserRequest};
|
||||||
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
|
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ async fn main() {
|
||||||
|
|
||||||
let bta = BlogdorTheAggregator::new().await;
|
let bta = BlogdorTheAggregator::new().await;
|
||||||
let (tx, rx) = unbounded_channel();
|
let (tx, rx) = unbounded_channel();
|
||||||
bta.spawn_http(tx, bta.client()).await;
|
bta.spawn_http(tx).await;
|
||||||
|
|
||||||
run_loop(&bta, rx).await;
|
run_loop(&bta, rx).await;
|
||||||
|
|
||||||
|
|
@ -31,40 +31,23 @@ fn init_logs() {
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_loop(bta: &BlogdorTheAggregator, mut announce_rx: UnboundedReceiver<NewFeed>) {
|
async fn run_loop(bta: &BlogdorTheAggregator, mut user_req_rx: UnboundedReceiver<UserRequest>) {
|
||||||
let mut check_feeds = tokio::time::interval(BLOGDOR_SNOOZE);
|
let mut check_feeds = tokio::time::interval(BLOGDOR_SNOOZE);
|
||||||
let mut check_stale = tokio::time::interval(Duration::from_hours(24));
|
let mut check_stale = tokio::time::interval(Duration::from_hours(24));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
announce = announce_rx.recv() => {
|
user_req = user_req_rx.recv() => {
|
||||||
if let Some(announce) = announce {
|
if let Some(ureq) = user_req {
|
||||||
bta.announce_feed(&announce).await;
|
bta.process_user_request(&ureq).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = check_feeds.tick() => {
|
_ = check_feeds.tick() => {
|
||||||
match bta.check_feeds().await {
|
match bta.check_feeds().await {
|
||||||
Ok(results) => {
|
Ok(results) => {
|
||||||
for result in results {
|
for result in results {
|
||||||
match result {
|
bta.post_entries(&result).await;
|
||||||
Ok(result) => {
|
|
||||||
if let Some(ref posts) = result.entries {
|
|
||||||
tracing::debug!(
|
|
||||||
"got {} new posts from {}",
|
|
||||||
posts.len(),
|
|
||||||
result.url
|
|
||||||
);
|
|
||||||
bta.post_entries(posts).await;
|
|
||||||
} else {
|
|
||||||
tracing::debug!("no new posts from {}", result.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// inner error for singular feed
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("could not check feed: {e}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// outer check_feeds error
|
// outer check_feeds error
|
||||||
|
|
|
||||||
192
src/server.rs
192
src/server.rs
|
|
@ -9,8 +9,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use sqlx::SqlitePool;
|
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use winnow::{
|
use winnow::{
|
||||||
Parser,
|
Parser,
|
||||||
|
|
@ -20,31 +19,21 @@ use winnow::{
|
||||||
token::{literal, take_until, take_while},
|
token::{literal, take_until, take_while},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::NewFeed;
|
use crate::{Action, FeedCommand, UserRequest};
|
||||||
|
|
||||||
type Payload = Map<String, Value>;
|
type Payload = Map<String, Value>;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
db: SqlitePool,
|
|
||||||
client: reqwest::Client,
|
|
||||||
email: String,
|
email: String,
|
||||||
token: String,
|
token: String,
|
||||||
announce_tx: UnboundedSender<NewFeed>,
|
user_request_tx: UnboundedSender<UserRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
pub fn new(
|
pub fn new(email: &str, token: &str, user_request_tx: UnboundedSender<UserRequest>) -> Self {
|
||||||
db: SqlitePool,
|
|
||||||
email: &str,
|
|
||||||
token: &str,
|
|
||||||
announce_tx: UnboundedSender<NewFeed>,
|
|
||||||
client: reqwest::Client,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
db,
|
user_request_tx,
|
||||||
client,
|
|
||||||
announce_tx,
|
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
token: token.to_string(),
|
token: token.to_string(),
|
||||||
}
|
}
|
||||||
|
|
@ -86,10 +75,7 @@ async fn handle_manage_feed(
|
||||||
|
|
||||||
if state.email == bot_email && state.token == token {
|
if state.email == bot_email && state.token == token {
|
||||||
let ZulipMessage {
|
let ZulipMessage {
|
||||||
content,
|
content, sender_id, ..
|
||||||
sender_id,
|
|
||||||
sender_full_name,
|
|
||||||
..
|
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
let mut resp: HashMap<&str, String> = HashMap::new();
|
let mut resp: HashMap<&str, String> = HashMap::new();
|
||||||
|
|
@ -111,32 +97,27 @@ async fn handle_manage_feed(
|
||||||
|
|
||||||
tracing::debug!(command = ?command);
|
tracing::debug!(command = ?command);
|
||||||
|
|
||||||
match command.action {
|
let (tx, mut rx) = unbounded_channel();
|
||||||
Action::Add => {
|
match state.user_request_tx.send(UserRequest {
|
||||||
match add_feed(&state.db, sender_id, command.feed, &state.client).await {
|
command,
|
||||||
Ok(_) => {
|
owner: sender_id,
|
||||||
let _ = state.announce_tx.send(NewFeed {
|
result_sender: tx,
|
||||||
feed: command.feed.to_string(),
|
}) {
|
||||||
user: sender_full_name,
|
Ok(_) => {}
|
||||||
});
|
Err(e) => {
|
||||||
resp.insert("content", "Blogdor Says: SUCCESS!".to_string());
|
tracing::error!("could not send add feed message to runloop: {e}");
|
||||||
}
|
resp.insert("content", "oh no, something terrible happened!".to_string());
|
||||||
Err(e) => {
|
|
||||||
resp.insert("content", format!("Blogdor Says: OH NO! {e}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Action::Remove => match remove_feed(&state.db, sender_id, command.feed).await {
|
}
|
||||||
Ok(_) => {
|
|
||||||
resp.insert("content", "Blogdor Says: BURNINATED!".to_string());
|
match rx.recv().await {
|
||||||
}
|
Some(Ok(r)) => {
|
||||||
Err(e) => {
|
resp.insert("content", format!("Blogdor Says: {r}"));
|
||||||
resp.insert("content", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Action::Help => {
|
|
||||||
resp.insert("content", "DM or `@blogdor's manager` with `add <feed url, RSS or Atom XML files>`, `remove <feed url originally added by you>`, or `help` to get this message (duh).".to_string());
|
|
||||||
}
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
resp.insert("content", format!("Blogdor Says: ERRORED! {e}"));
|
||||||
|
}
|
||||||
|
None => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
}
|
}
|
||||||
|
|
||||||
Json(resp).into_response()
|
Json(resp).into_response()
|
||||||
|
|
@ -145,100 +126,6 @@ async fn handle_manage_feed(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_feed(db: &SqlitePool, user: u32, feed: &str) -> Result<(), String> {
|
|
||||||
sqlx::query!(
|
|
||||||
"update feeds set active = false, updated_at = current_timestamp where url = ? and added_by = ?",
|
|
||||||
feed,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.execute(db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tracing::error!("could not set {feed} inactive by {user}, got {e}");
|
|
||||||
"sorry buddy, Blogdor couldn't do that".to_string()
|
|
||||||
})?;
|
|
||||||
sqlx::query!(
|
|
||||||
"select * from feeds where added_by = ? and active = false",
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.fetch_one(db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tracing::error!("could not set {feed} inactive by {user}, got {e}");
|
|
||||||
"sorry buddy, Blogdor couldn't do that".to_string()
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_feed(
|
|
||||||
db: &SqlitePool,
|
|
||||||
user: u32,
|
|
||||||
feed: &str,
|
|
||||||
client: &reqwest::Client,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
add_user(db, user).await?;
|
|
||||||
|
|
||||||
let _ = crate::fetch_and_parse_feed(feed, client).await?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
"update feeds set active = true, updated_at = current_timestamp where url = ? and added_by = ?",
|
|
||||||
feed,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.execute(db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Got error adding feed: {e}"))?;
|
|
||||||
|
|
||||||
if sqlx::query!(
|
|
||||||
"select * from feeds where added_by = ? and url = ? and active = true",
|
|
||||||
user,
|
|
||||||
feed
|
|
||||||
)
|
|
||||||
.fetch_optional(db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("{e}"))?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
"insert into feeds (url, added_by, active) values (?, ?, true)",
|
|
||||||
feed,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.execute(db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("{e}"))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_user(db: &SqlitePool, user: u32) -> Result<(), String> {
|
|
||||||
if let Err(e) = sqlx::query!("insert into users (zulip_id) values (?)", user)
|
|
||||||
.execute(db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
match e {
|
|
||||||
sqlx::Error::Database(database_error) => {
|
|
||||||
// the users table has only one constraint, which is a uniqueness one on
|
|
||||||
// zulip_id, so if it's violated, we don't care, it just means we already have
|
|
||||||
// that user; if it's not a constraint violation, then something
|
|
||||||
// else and bad has happened
|
|
||||||
if database_error.constraint().is_some() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sqlx::Error::Io(error) => {
|
|
||||||
tracing::error!("got IO error: {error}");
|
|
||||||
return Err("you should maybe retry that".to_string());
|
|
||||||
}
|
|
||||||
_ => return Err("yikes".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn graceful_shutdown(cancel: CancellationToken) {
|
async fn graceful_shutdown(cancel: CancellationToken) {
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
|
|
@ -278,25 +165,11 @@ struct ManageFeedMessage {
|
||||||
struct ZulipMessage {
|
struct ZulipMessage {
|
||||||
content: String,
|
content: String,
|
||||||
sender_id: u32,
|
sender_id: u32,
|
||||||
sender_full_name: String,
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
_rest: Payload,
|
_rest: Payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
fn parse_command(input: &mut &str) -> winnow::Result<Option<FeedCommand>> {
|
||||||
enum Action {
|
|
||||||
Add,
|
|
||||||
Remove,
|
|
||||||
Help,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
struct FeedCommand<'req> {
|
|
||||||
feed: &'req str,
|
|
||||||
action: Action,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_command<'i>(input: &mut &'i str) -> winnow::Result<Option<FeedCommand<'i>>> {
|
|
||||||
let s = take_until::<_, _, ()>(0.., "@**blogdor's manager**").parse_next(input);
|
let s = take_until::<_, _, ()>(0.., "@**blogdor's manager**").parse_next(input);
|
||||||
match s {
|
match s {
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
|
|
@ -315,8 +188,9 @@ fn parse_command<'i>(input: &mut &'i str) -> winnow::Result<Option<FeedCommand<'
|
||||||
"add",
|
"add",
|
||||||
"remove",
|
"remove",
|
||||||
"help",
|
"help",
|
||||||
|
"list",
|
||||||
fail.context(StrContext::Expected(StrContextValue::Description(
|
fail.context(StrContext::Expected(StrContextValue::Description(
|
||||||
"`add <feed url>`, `remove <feed url>`, or `help`",
|
"`add <feed url>`, `remove <feed url>`, `list`, or `help`",
|
||||||
))),
|
))),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
@ -324,6 +198,7 @@ fn parse_command<'i>(input: &mut &'i str) -> winnow::Result<Option<FeedCommand<'
|
||||||
"add" => Action::Add,
|
"add" => Action::Add,
|
||||||
"remove" => Action::Remove,
|
"remove" => Action::Remove,
|
||||||
"help" => Action::Help,
|
"help" => Action::Help,
|
||||||
|
"list" => Action::List,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
})
|
})
|
||||||
.parse_next(input)?;
|
.parse_next(input)?;
|
||||||
|
|
@ -336,7 +211,10 @@ fn parse_command<'i>(input: &mut &'i str) -> winnow::Result<Option<FeedCommand<'
|
||||||
)
|
)
|
||||||
.map(|(_, f, _, _)| f)
|
.map(|(_, f, _, _)| f)
|
||||||
.parse_next(input)?;
|
.parse_next(input)?;
|
||||||
Ok(Some(FeedCommand { feed, action }))
|
Ok(Some(FeedCommand {
|
||||||
|
feed: feed.to_string(),
|
||||||
|
action,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -360,7 +238,7 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
c,
|
c,
|
||||||
FeedCommand {
|
FeedCommand {
|
||||||
feed: "feed-url",
|
feed: "feed-url".to_string(),
|
||||||
action: Action::Add
|
action: Action::Add
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -371,7 +249,7 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
c,
|
c,
|
||||||
FeedCommand {
|
FeedCommand {
|
||||||
feed: "feed-url",
|
feed: "feed-url".to_string(),
|
||||||
action: Action::Remove
|
action: Action::Remove
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
1
src/test.rs
Normal file
1
src/test.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Loading…
Reference in a new issue