diff --git a/.tci b/.tci index 8f71056..7ff0ce0 100755 --- a/.tci +++ b/.tci @@ -7,13 +7,13 @@ systemctl --user stop upub echo "installing new binary" cp ./target/release/upub /opt/bin/upub echo "migrating database" -/opt/bin/upub --db "sqlite:///srv/tci/upub.db" --domain https://feditest.alemi.dev migrate +/opt/bin/upub --db "sqlite:///srv/tci/upub.db" --domain https://upub.alemi.dev migrate echo "restarting service" systemctl --user start upub echo "rebuilding frontend" cd web -CARGO_BUILD_JOBS=1 /opt/bin/trunk build --release --public-url 'https://feditest.alemi.dev/web' +CARGO_BUILD_JOBS=1 /opt/bin/trunk build --release --public-url 'https://upub.alemi.dev/web' echo "deploying frontend" -rm /srv/http/feditest/web/* -mv ./dist/* /srv/http/feditest/web/ +rm /srv/http/upub/web/* +mv ./dist/* /srv/http/upub/web/ echo "done" diff --git a/Cargo.lock b/Cargo.lock index e8d3f0e..7dcf1e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,47 +79,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -127,9 +128,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "apb" @@ -153,13 +160,13 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-recursion" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -181,7 +188,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -192,7 +199,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -204,6 +211,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attribute-derive" version = "0.9.1" @@ -215,7 +228,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -231,14 +244,14 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -318,9 +331,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -401,9 +414,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" dependencies = [ "borsh-derive", "cfg_aliases", @@ -411,15 +424,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "syn_derive", ] @@ -465,15 +478,15 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" [[package]] name = "cc" -version = "1.0.94" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -561,7 +574,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -578,9 +591,9 @@ checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "comrak" @@ -627,16 +640,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "console_log" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" -dependencies = [ - "log", - "web-sys", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -725,9 +728,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -743,9 +746,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -785,12 +788,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", + "darling_core 0.20.9", + "darling_macro 0.20.9", ] [[package]] @@ -823,16 +826,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.60", + "strsim 0.11.1", + "syn 2.0.66", ] [[package]] @@ -859,13 +862,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ - "darling_core 0.20.8", + "darling_core 0.20.9", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -875,7 +878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -887,10 +890,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fa90da96b8fd491f5754d1f7a731f73921e3b7aa0ce333c821a0e43666ac14" dependencies = [ - "darling 0.20.8", + "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -933,7 +936,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -969,9 +972,9 @@ dependencies = [ [[package]] name = "deunicode" -version = "1.4.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" [[package]] name = "digest" @@ -999,9 +1002,9 @@ checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] @@ -1029,18 +1032,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" dependencies = [ "serde", + "typeid", ] [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1075,15 +1079,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" - -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" @@ -1219,7 +1217,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1264,9 +1262,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -1288,15 +1286,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "gloo-net" -version = "0.5.0" +name = "gloo" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net 0.3.1", + "gloo-render", + "gloo-storage", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.1.7", "http 0.2.12", "js-sys", "pin-project", @@ -1308,6 +1386,62 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1320,6 +1454,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -1334,16 +1481,33 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.4.4" +name = "gloo-worker" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" dependencies = [ + "anymap2", + "bincode", + "gloo-console", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http 1.1.0", "indexmap", "slab", @@ -1373,9 +1537,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", "allocator-api2", @@ -1387,7 +1551,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1464,7 +1628,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1563,9 +1727,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" dependencies = [ "bytes", "futures-channel", @@ -1636,7 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1647,7 +1811,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1668,6 +1832,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.12.1" @@ -1733,17 +1903,17 @@ dependencies = [ [[package]] name = "leptos-use" -version = "0.10.7" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6be6f1d02c3277f03a55c4a05ee5a069ea75ec5323c4415f6efed69cb5acea" +checksum = "3272d90b77cdbb99e9060f90eb6f5738e56128b2f912db57a50efb006a26e262" dependencies = [ "async-trait", "cfg-if", "cookie", "default-struct-builder", "futures-util", - "gloo-timers", - "gloo-utils", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", "js-sys", "lazy_static", "leptos", @@ -1813,7 +1983,7 @@ dependencies = [ "quote", "rstml", "serde", - "syn 2.0.60", + "syn 2.0.66", "walkdir", ] @@ -1835,7 +2005,7 @@ dependencies = [ "quote", "rstml", "server_fn_macro", - "syn 2.0.60", + "syn 2.0.66", "tracing", "uuid", ] @@ -1846,7 +2016,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057de706568ce8f1f223ae69f796c10ad0563ad270d10717e70c2b2d22eefa60" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "cfg-if", "futures", "indexmap", @@ -1856,7 +2026,7 @@ dependencies = [ "rustc-hash", "self_cell", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.6.5", "serde_json", "slotmap", "thiserror", @@ -1873,7 +2043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7fcc2a95a20c8f41adb39770e65c48ffe33cd9503b83669c54edd9b33ba8aa8" dependencies = [ "cfg-if", - "gloo-net", + "gloo-net 0.5.0", "itertools", "js-sys", "lazy_static", @@ -1910,9 +2080,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -1955,15 +2125,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1994,7 +2164,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2090,9 +2260,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -2164,11 +2334,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2207,9 +2376,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2218,9 +2387,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2296,7 +2465,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2347,7 +2516,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2364,9 +2533,9 @@ checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2374,22 +2543,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -2486,7 +2655,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2562,12 +2731,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2616,9 +2785,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -2631,7 +2800,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "version_check", "yansi", ] @@ -2694,7 +2863,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2742,6 +2911,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -2797,11 +2975,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2910,7 +3088,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.60", + "syn 2.0.66", "syn_derive", "thiserror", ] @@ -2933,9 +3111,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -2945,9 +3123,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2958,9 +3136,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "ring", "rustls-webpki", @@ -2982,15 +3160,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -3004,15 +3182,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3058,7 +3236,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3116,7 +3294,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.60", + "syn 2.0.66", "unicode-ident", ] @@ -3180,7 +3358,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "thiserror", ] @@ -3215,11 +3393,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -3228,9 +3406,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -3238,9 +3416,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "send_wrapper" @@ -3253,9 +3431,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.198" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -3268,7 +3446,18 @@ checksum = "9980133dc534d02ab08df3b384295223a45090c40a4c46240e3eaa982b495910" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", ] [[package]] @@ -3296,13 +3485,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3316,9 +3505,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -3348,9 +3537,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -3387,7 +3576,7 @@ dependencies = [ "const_format", "dashmap", "futures", - "gloo-net", + "gloo-net 0.5.0", "http 1.1.0", "js-sys", "once_cell", @@ -3415,7 +3604,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "xxhash-rust", ] @@ -3426,7 +3615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4ad11700cbccdbd313703916eb8c97301ee423c4a06e5421b77956fdcb36a9f" dependencies = [ "server_fn_macro", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3481,9 +3670,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3547,9 +3736,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3841,13 +4030,13 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -3965,9 +4154,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -3983,7 +4172,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4083,22 +4272,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4157,6 +4346,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tld" +version = "2.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ca5fc340fcb4f52570c502cf443fc22d5521e9ef2bb03528e3634254016cf7" +dependencies = [ + "phf", + "phf_codegen", +] + [[package]] name = "tokio" version = "1.37.0" @@ -4184,7 +4383,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4210,35 +4409,34 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.13", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -4256,15 +4454,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.8", ] [[package]] @@ -4332,7 +4530,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4374,6 +4572,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-subscriber-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79804e80980173c6c8e53d98508eb24a2dbc4ee17a3e8d2ca8e5bad6bf13a898" +dependencies = [ + "gloo", + "tracing", + "tracing-subscriber", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4403,9 +4612,15 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] +[[package]] +name = "typeid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" + [[package]] name = "typenum" version = "1.17.0" @@ -4433,6 +4648,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -4464,7 +4685,7 @@ dependencies = [ "apb", "async-recursion", "axum", - "base64 0.22.0", + "base64 0.22.1", "chrono", "clap", "futures", @@ -4474,6 +4695,7 @@ dependencies = [ "nodeinfo", "openssl", "rand", + "regex", "reqwest", "sea-orm", "sea-orm-migration", @@ -4500,9 +4722,9 @@ dependencies = [ "apb", "chrono", "console_error_panic_hook", - "console_log", "dashmap", "futures", + "jrd", "lazy_static", "leptos", "leptos-use", @@ -4515,7 +4737,10 @@ dependencies = [ "serde_default", "serde_json", "thiserror", + "tld", "tracing", + "tracing-subscriber", + "tracing-subscriber-wasm", "uriproxy", "web-sys", ] @@ -4524,7 +4749,7 @@ dependencies = [ name = "uriproxy" version = "0.1.0" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", ] [[package]] @@ -4580,9 +4805,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -4590,9 +4815,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc35703541cbccb5278ef7b589d79439fc808ff0b5867195a3230f9a47421d39" +checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" dependencies = [ "erased-serde", "serde", @@ -4601,9 +4826,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "285b43c29d0b4c0e65aad24561baee67a1b69dc9be9375d4a85138cbf556f7f8" +checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" dependencies = [ "sval", "sval_buffer", @@ -4678,7 +4903,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -4712,7 +4937,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4758,7 +4983,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "redox_syscall", + "redox_syscall 0.4.1", "wasite", ] @@ -4780,11 +5005,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4952,9 +5177,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -5007,26 +5232,26 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/README.md b/README.md index 0d4e069..58719a4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ all interactions happen with ActivityPub's client-server methods (basically POST development is still active, so expect more stuff to come! since most fediverse software uses Mastodon's API, μpub plans to implement it as an optional feature, becoming eventually compatible with most existing frontends and mobile applications, but focus right now is on producing something specific to μpub needs -a test instance is _usually_ available at [feditest.alemi.dev](https://feditest.alemi.dev) +a test instance is _usually_ available at [upub.alemi.dev](https://upub.alemi.dev) ## about the database schema im going to be very real i tried to do migrations but its getting super messy so until further notice assume db to be volatile. next change may be a migration (easy!) or a whole db rebuild (aaaaaaaaaa...), so if you're not comfortable with either manually exporting/importing or dropping and starting from scratch, **you really shouldn't put upub in prod yet**! @@ -62,18 +62,19 @@ don't hesitate to get in touch, i'd be thrilled to showcase the project to you! - [x] like, share, reply via frontend - [x] backend config - [x] frontend config + - [x] optimize `addressing` database schema - [ ] mentions, notifications + - [ ] hashtags + - [ ] public vs unlisted for discovery - [ ] mastodon-like search bar - [ ] polls - [ ] better editing via web frontend - [ ] remote media proxy - [ ] upload media - - [ ] hashtags - - [ ] public vs unlisted for discovery - [ ] user fields - [ ] lists - [ ] full mastodon api - - [ ] optimize `addressing` database schema + - [ ] get rid of internal ids from code ## what about the name? μpub (or simply `upub`) means "[micro](https://en.wikipedia.org/wiki/International_System_of_Units#Prefixes)-pub", but could also be read "upub", "you-pub" or "mu-pub" diff --git a/apb/src/macros.rs b/apb/src/macros.rs index bc29122..2d05f3e 100644 --- a/apb/src/macros.rs +++ b/apb/src/macros.rs @@ -357,49 +357,3 @@ pub fn set_maybe_value(obj: &mut serde_json::Value, key: &str, value: Option); - fn insert_str(&mut self, k: &str, v: Option<&str>); - fn insert_float(&mut self, k: &str, f: Option); - fn insert_timestr(&mut self, k: &str, t: Option>); -} - -#[cfg(feature = "unstructured")] -impl InsertValue for serde_json::Map { - fn insert_node(&mut self, k: &str, node: crate::Node) { - if !node.is_nothing() { - self.insert(k.to_string(), node.into()); - } - } - - fn insert_str(&mut self, k: &str, v: Option<&str>) { - if let Some(v) = v { - self.insert( - k.to_string(), - serde_json::Value::String(v.to_string()), - ); - } - } - - fn insert_float(&mut self, k: &str, v: Option) { - if let Some(v) = v { - if let Some(n) = serde_json::Number::from_f64(v) { - self.insert( - k.to_string(), - serde_json::Value::Number(n), - ); - } - } - } - - fn insert_timestr(&mut self, k: &str, t: Option>) { - if let Some(published) = t { - self.insert( - k.to_string(), - serde_json::Value::String(published.to_rfc3339()), - ); - } - } -} diff --git a/src/cli/faker.rs b/src/cli/faker.rs index 5d8899e..5def164 100644 --- a/src/cli/faker.rs +++ b/src/cli/faker.rs @@ -1,16 +1,17 @@ -use crate::model::{addressing, config, credential, activity, object, user, Audience}; +use crate::model::{addressing, config, credential, activity, object, actor, Audience}; use openssl::rsa::Rsa; -use sea_orm::IntoActiveModel; +use sea_orm::{ActiveValue::NotSet, IntoActiveModel}; -pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_orm::DbErr> { +pub async fn faker(ctx: crate::server::Context, count: i64) -> Result<(), sea_orm::DbErr> { use sea_orm::{EntityTrait, Set}; let domain = ctx.domain(); let db = ctx.db(); let key = Rsa::generate(2048).unwrap(); - let test_user = user::Model { - id: format!("{domain}/users/test"), + let test_user = actor::Model { + internal: 42, + id: format!("{domain}/actors/test"), name: Some("μpub".into()), domain: clean_domain(domain), preferred_username: "test".to_string(), @@ -19,24 +20,25 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or following_count: 0, followers: None, followers_count: 0, - statuses_count: count as i64, + statuses_count: count as i32, icon: Some("https://cdn.alemi.dev/social/circle-square.png".to_string()), image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()), inbox: None, shared_inbox: None, outbox: None, actor_type: apb::ActorType::Person, - created: chrono::Utc::now(), + published: chrono::Utc::now(), updated: chrono::Utc::now(), private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()), // TODO generate a fresh one every time public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(), }; - user::Entity::insert(test_user.clone().into_active_model()).exec(db).await?; + actor::Entity::insert(test_user.clone().into_active_model()).exec(db).await?; config::Entity::insert(config::ActiveModel { - id: Set(test_user.id.clone()), + internal: NotSet, + actor: Set(test_user.id.clone()), accept_follow_requests: Set(true), show_followers: Set(true), show_following: Set(true), @@ -45,8 +47,9 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or }).exec(db).await?; credential::Entity::insert(credential::ActiveModel { - id: Set(test_user.id.clone()), - email: Set("mail@example.net".to_string()), + internal: NotSet, + actor: Set(test_user.id.clone()), + login: Set("mail@example.net".to_string()), password: Set(sha256::digest("very-strong-password")), }).exec(db).await?; @@ -57,28 +60,29 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or let aid = uuid::Uuid::new_v4(); addressing::Entity::insert(addressing::ActiveModel { - actor: Set(apb::target::PUBLIC.to_string()), - server: Set("www.w3.org".to_string()), - activity: Set(Some(format!("{domain}/activities/{aid}"))), - object: Set(Some(format!("{domain}/objects/{oid}"))), + actor: Set(None), + instance: Set(None), + activity: Set(Some(42 + i)), + object: Set(Some(42 + i)), published: Set(chrono::Utc::now()), ..Default::default() }).exec(db).await?; object::Entity::insert(object::ActiveModel { + internal: Set(42 + i), id: Set(format!("{domain}/objects/{oid}")), name: Set(None), object_type: Set(apb::ObjectType::Note), - attributed_to: Set(Some(format!("{domain}/users/test"))), + attributed_to: Set(Some(format!("{domain}/actors/test"))), summary: Set(None), context: Set(Some(context.clone())), in_reply_to: Set(None), content: Set(Some(format!("[{i}] Tic(k). Quasiparticle of intensive multiplicity. Tics (or ticks) are intrinsically several components of autonomously numbering anorganic populations, propagating by contagion between segmentary divisions in the order of nature. Ticks - as nonqualitative differentially-decomposable counting marks - each designate a multitude comprehended as a singular variation in tic(k)-density."))), - published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), - updated: Set(None), - comments: Set(0), + published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i as u64)), + updated: Set(chrono::Utc::now()), + replies: Set(0), likes: Set(0), - shares: Set(0), + announces: Set(0), to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), bto: Set(Audience::default()), cc: Set(Audience(vec![])), @@ -88,12 +92,13 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or }).exec(db).await?; activity::Entity::insert(activity::ActiveModel { + internal: Set(42 + i), id: Set(format!("{domain}/activities/{aid}")), activity_type: Set(apb::ActivityType::Create), - actor: Set(format!("{domain}/users/test")), + actor: Set(format!("{domain}/actors/test")), object: Set(Some(format!("{domain}/objects/{oid}"))), target: Set(None), - published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), + published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i as u64)), to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), bto: Set(Audience::default()), cc: Set(Audience(vec![])), diff --git a/src/cli/fetch.rs b/src/cli/fetch.rs index 0f1c4f7..a589e6b 100644 --- a/src/cli/fetch.rs +++ b/src/cli/fetch.rs @@ -1,6 +1,6 @@ -use sea_orm::{EntityTrait, IntoActiveModel}; +use sea_orm::EntityTrait; -use crate::server::fetcher::Fetchable; +use crate::server::{fetcher::Fetchable, normalizer::Normalizer, Context}; pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crate::Result<()> { use apb::Base; @@ -8,24 +8,23 @@ pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crat let mut node = apb::Node::link(uri.to_string()); node.fetch(&ctx).await?; - let obj = node.get().expect("node still empty after fetch?"); + let obj = node.extract().expect("node still empty after fetch?"); + let server = Context::server(&uri); + + println!("{}", serde_json::to_string_pretty(&obj).unwrap()); if save { match obj.base_type() { Some(apb::BaseType::Object(apb::ObjectType::Actor(_))) => { - crate::model::user::Entity::insert( - crate::model::user::Model::new(obj).unwrap().into_active_model() + crate::model::actor::Entity::insert( + crate::model::actor::ActiveModel::new(&obj).unwrap() ).exec(ctx.db()).await.unwrap(); }, Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => { - crate::model::activity::Entity::insert( - crate::model::activity::Model::new(obj).unwrap().into_active_model() - ).exec(ctx.db()).await.unwrap(); + ctx.insert_activity(obj, Some(server)).await.unwrap(); }, Some(apb::BaseType::Object(apb::ObjectType::Note)) => { - crate::model::object::Entity::insert( - crate::model::object::Model::new(obj).unwrap().into_active_model() - ).exec(ctx.db()).await.unwrap(); + ctx.insert_object(obj, Some(server)).await.unwrap(); }, Some(apb::BaseType::Object(t)) => tracing::warn!("not implemented: {:?}", t), Some(apb::BaseType::Link(_)) => tracing::error!("fetched another link?"), @@ -33,7 +32,5 @@ pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crat } } - println!("{}", serde_json::to_string_pretty(&obj).unwrap()); - Ok(()) } diff --git a/src/cli/fix.rs b/src/cli/fix.rs index 9ad85d8..53ede8e 100644 --- a/src/cli/fix.rs +++ b/src/cli/fix.rs @@ -11,13 +11,13 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies { let mut stream = crate::model::like::Entity::find().stream(db).await?; while let Some(like) = stream.try_next().await? { - store.insert(like.likes.clone(), store.get(&like.likes).unwrap_or(&0) + 1); + store.insert(like.object, store.get(&like.object).unwrap_or(&0) + 1); } } for (k, v) in store { let m = crate::model::object::ActiveModel { - id: sea_orm::Set(k.clone()), + internal: sea_orm::Set(k), likes: sea_orm::Set(v), ..Default::default() }; @@ -34,16 +34,16 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies tracing::info!("fixing shares..."); let mut store = std::collections::HashMap::new(); { - let mut stream = crate::model::share::Entity::find().stream(db).await?; + let mut stream = crate::model::announce::Entity::find().stream(db).await?; while let Some(share) = stream.try_next().await? { - store.insert(share.shares.clone(), store.get(&share.shares).unwrap_or(&0) + 1); + store.insert(share.object, store.get(&share.object).unwrap_or(&0) + 1); } } for (k, v) in store { let m = crate::model::object::ActiveModel { - id: sea_orm::Set(k.clone()), - shares: sea_orm::Set(v), + internal: sea_orm::Set(k), + announces: sea_orm::Set(v), ..Default::default() }; if let Err(e) = crate::model::object::Entity::update(m) @@ -71,7 +71,7 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies for (k, v) in store { let m = crate::model::object::ActiveModel { id: sea_orm::Set(k.clone()), - comments: sea_orm::Set(v), + replies: sea_orm::Set(v), ..Default::default() }; if let Err(e) = crate::model::object::Entity::update(m) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 293646e..bcf8eea 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -104,7 +104,7 @@ pub async fn run( ).await?; match command { CliCommand::Faker { count } => - Ok(faker(ctx, count).await?), + Ok(faker(ctx, count as i64).await?), CliCommand::Fetch { uri, save } => Ok(fetch(ctx, uri, save).await?), CliCommand::Relay { actor, accept } => diff --git a/src/cli/relay.rs b/src/cli/relay.rs index 850a84f..d618229 100644 --- a/src/cli/relay.rs +++ b/src/cli/relay.rs @@ -1,19 +1,22 @@ -use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder}; +use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; + +use crate::server::addresser::Addresser; pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) -> crate::Result<()> { let aid = ctx.aid(&uuid::Uuid::new_v4().to_string()); - let mut activity_model = crate::model::activity::Model { - id: aid.clone(), - activity_type: apb::ActivityType::Follow, - actor: ctx.base().to_string(), - object: Some(actor.clone()), - target: None, - published: chrono::Utc::now(), - to: crate::model::Audience(vec![actor.clone()]), - bto: crate::model::Audience::default(), - cc: crate::model::Audience(vec![apb::target::PUBLIC.to_string()]), - bcc: crate::model::Audience::default(), + let mut activity_model = crate::model::activity::ActiveModel { + internal: NotSet, + id: Set(aid.clone()), + activity_type: Set(apb::ActivityType::Follow), + actor: Set(ctx.base().to_string()), + object: Set(Some(actor.clone())), + target: Set(None), + published: Set(chrono::Utc::now()), + to: Set(crate::model::Audience(vec![actor.clone()])), + bto: Set(crate::model::Audience::default()), + cc: Set(crate::model::Audience(vec![apb::target::PUBLIC.to_string()])), + bcc: Set(crate::model::Audience::default()), }; if accept { @@ -25,11 +28,11 @@ pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) -> .one(ctx.db()) .await? .expect("no follow request to accept"); - activity_model.activity_type = apb::ActivityType::Accept(apb::AcceptType::Accept); - activity_model.object = Some(follow_req.id); + activity_model.activity_type = Set(apb::ActivityType::Accept(apb::AcceptType::Accept)); + activity_model.object = Set(Some(follow_req.id)); }; - crate::model::activity::Entity::insert(activity_model.into_active_model()) + crate::model::activity::Entity::insert(activity_model) .exec(ctx.db()).await?; ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?; diff --git a/src/cli/update.rs b/src/cli/update.rs index d5318ce..9d75789 100644 --- a/src/cli/update.rs +++ b/src/cli/update.rs @@ -1,5 +1,5 @@ use futures::TryStreamExt; -use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; +use sea_orm::{ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter}; use crate::server::fetcher::Fetcher; @@ -8,28 +8,35 @@ pub async fn update_users(ctx: crate::server::Context, days: i64) -> crate::Resu let mut insertions = Vec::new(); { - let mut stream = crate::model::user::Entity::find() - .filter(crate::model::user::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days))) + let mut stream = crate::model::actor::Entity::find() + .filter(crate::model::actor::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days))) .stream(ctx.db()) .await?; while let Some(user) = stream.try_next().await? { if ctx.is_local(&user.id) { continue } - match ctx.pull_user(&user.id).await { + match ctx.pull(&user.id).await.map(|x| x.actor()) { Err(e) => tracing::warn!("could not update user {}: {e}", user.id), - Ok(u) => { - insertions.push(u); - count += 1; + Ok(Err(e)) => tracing::warn!("could not update user {}: {e}", user.id), + Ok(Ok(doc)) => match crate::model::actor::ActiveModel::new(&doc) { + Ok(mut u) => { + u.internal = Set(user.internal); + u.updated = Set(chrono::Utc::now()); + insertions.push((user.id, u)); + count += 1; + }, + Err(e) => tracing::warn!("failed deserializing user '{}': {e}", user.id), }, } } } - for u in insertions { - tracing::info!("updating user {}", u.id); - crate::model::user::Entity::delete_by_id(&u.id).exec(ctx.db()).await?; - crate::model::user::Entity::insert(u.into_active_model()).exec(ctx.db()).await?; + for (uid, user_model) in insertions { + tracing::info!("updating user {}", uid); + crate::model::actor::Entity::update(user_model) + .exec(ctx.db()) + .await?; } tracing::info!("updated {count} users"); diff --git a/src/config.rs b/src/config.rs index aba6a41..a040a30 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,15 @@ pub struct SecurityConfig { #[serde_inline_default(true)] pub show_reply_ids: bool, + + #[serde(default)] + pub allow_login_refresh: bool, + + #[serde_inline_default(2)] + pub max_id_redirects: u32, + + #[serde_inline_default(20)] + pub thread_crawl_depth: u32, } diff --git a/src/errors.rs b/src/errors.rs index 446fdbd..1d0862f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -28,6 +28,9 @@ pub enum UpubError { #[error("invalid base64 string: {0:?}")] Base64(#[from] base64::DecodeError), + #[error("type mismatch on object: expected {0:?}, found {1:?}")] + Mismatch(apb::ObjectType, apb::ObjectType), + // TODO this isn't really an error but i need to redirect from some routes so this allows me to // keep the type hints on the return type, still what the hell!!!! #[error("redirecting to {0}")] @@ -62,6 +65,10 @@ impl UpubError { pub fn internal_server_error() -> Self { Self::Status(axum::http::StatusCode::INTERNAL_SERVER_ERROR) } + + pub fn field(field: &'static str) -> Self { + Self::Field(crate::model::FieldError(field)) + } } pub type UpubResult = Result; @@ -74,6 +81,9 @@ impl From for UpubError { impl axum::response::IntoResponse for UpubError { fn into_response(self) -> axum::response::Response { + // TODO it's kind of jank to hide this print down here, i should probably learn how spans work + // in tracing and use the library's features but ehhhh + tracing::debug!("emitting error response: {self:?}"); match self { UpubError::Redirect(to) => Redirect::to(&to).into_response(), UpubError::Status(status) => status.into_response(), @@ -101,6 +111,15 @@ impl axum::response::IntoResponse for UpubError { "description": format!("missing required field from request: '{}'", x.0), })) ).into_response(), + UpubError::Mismatch(expected, found) => ( + axum::http::StatusCode::UNPROCESSABLE_ENTITY, + axum::Json(serde_json::json!({ + "error": "type", + "expected": expected.as_ref().to_string(), + "found": found.as_ref().to_string(), + "description": self.to_string(), + })) + ).into_response(), _ => ( StatusCode::INTERNAL_SERVER_ERROR, axum::Json(serde_json::json!({ diff --git a/src/main.rs b/src/main.rs index 51d1065..af04db9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ -pub mod server; // TODO there are some methods that i dont use yet, make it public so that ra shuts up +mod server; mod model; mod routes; -mod errors; +pub mod errors; mod config; #[cfg(feature = "cli")] @@ -51,7 +51,11 @@ struct Args { #[derive(Clone, Subcommand)] enum Mode { /// run fediverse server - Serve, + Serve { + #[arg(short, long, default_value="127.0.0.1:3000")] + /// addr to bind and serve onto + bind: String, + }, /// print current or default configuration Config, @@ -88,13 +92,14 @@ async fn main() { let mut opts = ConnectOptions::new(&database); opts + .sqlx_logging(true) .sqlx_logging_level(tracing::log::LevelFilter::Debug) .max_connections(config.datasource.max_connections) .min_connections(config.datasource.min_connections) .acquire_timeout(std::time::Duration::from_secs(config.datasource.acquire_timeout_seconds)) .connect_timeout(std::time::Duration::from_secs(config.datasource.connect_timeout_seconds)) .sqlx_slow_statements_logging_settings( - if config.datasource.slow_query_warn_enable { tracing::log::LevelFilter::Warn } else { tracing::log::LevelFilter::Off }, + if config.datasource.slow_query_warn_enable { tracing::log::LevelFilter::Warn } else { tracing::log::LevelFilter::Debug }, std::time::Duration::from_secs(config.datasource.slow_query_warn_seconds) ); @@ -114,7 +119,7 @@ async fn main() { Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")), - Mode::Serve => { + Mode::Serve { bind } => { let ctx = server::Context::new(db, domain, config) .await.expect("failed creating server context"); @@ -129,7 +134,7 @@ async fn main() { .with_state(ctx); // run our app with hyper, listening locally on port 3000 - let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") + let listener = tokio::net::TcpListener::bind(bind) .await.expect("could not bind tcp socket"); axum::serve(listener, router) diff --git a/src/migrations/m20240316_000001_create_table.rs b/src/migrations/m20240316_000001_create_table.rs deleted file mode 100644 index 8fbcdc7..0000000 --- a/src/migrations/m20240316_000001_create_table.rs +++ /dev/null @@ -1,169 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Users::Table) - .col( - ColumnDef::new(Users::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Users::ActorType).string().not_null()) - .col(ColumnDef::new(Users::Domain).string().not_null()) - .col(ColumnDef::new(Users::Name).string().null()) - .col(ColumnDef::new(Users::Summary).string().null()) - .col(ColumnDef::new(Users::Image).string().null()) - .col(ColumnDef::new(Users::Icon).string().null()) - .col(ColumnDef::new(Users::PreferredUsername).string().not_null()) - .col(ColumnDef::new(Users::Inbox).string().null()) - .col(ColumnDef::new(Users::SharedInbox).string().null()) - .col(ColumnDef::new(Users::Outbox).string().null()) - .col(ColumnDef::new(Users::Following).string().null()) - .col(ColumnDef::new(Users::Followers).string().null()) - .col(ColumnDef::new(Users::FollowingCount).integer().not_null().default(0)) - .col(ColumnDef::new(Users::FollowersCount).integer().not_null().default(0)) - // .col(ColumnDef::new(Users::StatusesCount).integer().not_null().default(0)) - .col(ColumnDef::new(Users::PublicKey).string().not_null()) - .col(ColumnDef::new(Users::PrivateKey).string().null()) - .col(ColumnDef::new(Users::Created).date_time().not_null()) - .col(ColumnDef::new(Users::Updated).date_time().not_null()) - .to_owned() - ) - .await?; - - manager - .create_table( - Table::create() - .table(Activities::Table) - .col( - ColumnDef::new(Activities::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Activities::ActivityType).string().not_null()) - .col(ColumnDef::new(Activities::Actor).string().not_null()) - .col(ColumnDef::new(Activities::Object).string().null()) - .col(ColumnDef::new(Activities::Target).string().null()) - .col(ColumnDef::new(Activities::To).json().null()) - .col(ColumnDef::new(Activities::Bto).json().null()) - .col(ColumnDef::new(Activities::Cc).json().null()) - .col(ColumnDef::new(Activities::Bcc).json().null()) - .col(ColumnDef::new(Activities::Published).date_time().not_null()) - .to_owned() - ).await?; - - manager - .create_table( - Table::create() - .table(Objects::Table) - .col( - ColumnDef::new(Objects::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Objects::ObjectType).string().not_null()) - .col(ColumnDef::new(Objects::AttributedTo).string().null()) - .col(ColumnDef::new(Objects::Name).string().null()) - .col(ColumnDef::new(Objects::Summary).string().null()) - .col(ColumnDef::new(Objects::Content).string().null()) - .col(ColumnDef::new(Objects::Likes).integer().not_null().default(0)) - .col(ColumnDef::new(Objects::Shares).integer().not_null().default(0)) - .col(ColumnDef::new(Objects::Comments).integer().not_null().default(0)) - .col(ColumnDef::new(Objects::Context).string().null()) - .col(ColumnDef::new(Objects::To).json().null()) - .col(ColumnDef::new(Objects::Bto).json().null()) - .col(ColumnDef::new(Objects::Cc).json().null()) - .col(ColumnDef::new(Objects::Bcc).json().null()) - .col(ColumnDef::new(Objects::Published).string().not_null()) - .to_owned() - ).await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Users::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(Activities::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(Objects::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Users { - Table, - Id, - Domain, - ActorType, - Name, - Summary, - Image, - Icon, - PreferredUsername, - Inbox, - SharedInbox, - Outbox, - Following, - FollowingCount, - Followers, - FollowersCount, - // StatusesCount, - PublicKey, - PrivateKey, - Created, - Updated, -} - -#[derive(DeriveIden)] -enum Activities { - Table, - Id, - ActivityType, - Actor, - Object, - Target, - Cc, - Bcc, - To, - Bto, - Published, -} - -#[derive(DeriveIden)] -enum Objects { - Table, - Id, - ObjectType, - AttributedTo, - Name, - Summary, - Content, - Likes, - Shares, - Comments, - Context, - Cc, - Bcc, - To, - Bto, - Published, -} diff --git a/src/migrations/m20240322_000001_create_relations.rs b/src/migrations/m20240322_000001_create_relations.rs deleted file mode 100644 index e797c4d..0000000 --- a/src/migrations/m20240322_000001_create_relations.rs +++ /dev/null @@ -1,44 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Relations::Table) - .col( - ColumnDef::new(Relations::Id) - .integer() - .auto_increment() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Relations::Follower).string().not_null()) - .col(ColumnDef::new(Relations::Following).string().not_null()) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Relations::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Relations { - Table, - Id, - Follower, - Following, -} diff --git a/src/migrations/m20240322_000002_add_likes_shares.rs b/src/migrations/m20240322_000002_add_likes_shares.rs deleted file mode 100644 index 54361a8..0000000 --- a/src/migrations/m20240322_000002_add_likes_shares.rs +++ /dev/null @@ -1,80 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Likes::Table) - .col( - ColumnDef::new(Likes::Id) - .integer() - .auto_increment() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Likes::Actor).string().not_null()) - .col(ColumnDef::new(Likes::Likes).string().not_null()) - .col(ColumnDef::new(Likes::Date).date_time().not_null()) - .to_owned() - ) - .await?; - - manager - .create_table( - Table::create() - .table(Shares::Table) - .col( - ColumnDef::new(Shares::Id) - .integer() - .auto_increment() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Shares::Actor).string().not_null()) - .col(ColumnDef::new(Shares::Shares).string().not_null()) - .col(ColumnDef::new(Shares::Date).date_time().not_null()) - .to_owned() - ) - .await?; - - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Likes::Table).to_owned()) - .await?; - - manager - .drop_table(Table::drop().table(Shares::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -#[allow(clippy::enum_variant_names)] -enum Likes { - Table, - Id, - Actor, - Likes, - Date, -} - -#[derive(DeriveIden)] -#[allow(clippy::enum_variant_names)] -enum Shares { - Table, - Id, - Actor, - Shares, - Date, -} diff --git a/src/migrations/m20240322_000003_add_indexes.rs b/src/migrations/m20240322_000003_add_indexes.rs deleted file mode 100644 index 3469e4a..0000000 --- a/src/migrations/m20240322_000003_add_indexes.rs +++ /dev/null @@ -1,176 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_index( - Index::create() - .name("user-domain-index") - .table(Users::Table) - .col(Users::Domain) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("activities-published-descending-index") - .table(Activities::Table) - .col((Activities::Published, IndexOrder::Desc)) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("activities-actor-index") - .table(Activities::Table) - .col(Activities::Actor) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("activities-object-index") - .table(Activities::Table) - .col(Activities::Object) - .to_owned() - ).await?; - - manager - .create_index( - Index::create() - .name("objects-attributed-to-index") - .table(Objects::Table) - .col(Objects::AttributedTo) - .to_owned() - ).await?; - - manager - .create_index( - Index::create() - .name("shares-actor-index") - .table(Shares::Table) - .col(Shares::Actor) - .to_owned() - ).await?; - - manager - .create_index( - Index::create() - .name("shares-shares-index") - .table(Shares::Table) - .col(Shares::Shares) - .to_owned() - ).await?; - - manager - .create_index( - Index::create() - .name("likes-actor-index") - .table(Likes::Table) - .col(Likes::Actor) - .to_owned() - ).await?; - - manager - .create_index( - Index::create() - .name("likes-likes-index") - .table(Likes::Table) - .col(Likes::Likes) - .to_owned() - ).await?; - - manager - .create_index( - Index::create() - .name("likes-actor-likes-index") - .table(Likes::Table) - .col(Likes::Actor) - .col(Likes::Likes) - .unique() - .to_owned() - ).await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_index(Index::drop().name("user-domain-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("activities-published-descending-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("activities-actor-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("activities-object-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("objects-attributed-to-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("shares-actor-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("shares-shares-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("likes-actor-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("likes-likes-index").to_owned()) - .await?; - manager - .drop_index(Index::drop().name("likes-actor-likes-index").to_owned()) - .await?; - Ok(()) - } -} - -#[derive(DeriveIden)] -#[allow(clippy::enum_variant_names)] -enum Likes { - Table, - Actor, - Likes, -} - -#[derive(DeriveIden)] -#[allow(clippy::enum_variant_names)] -enum Shares { - Table, - Actor, - Shares, -} - -#[derive(DeriveIden)] -enum Users { - Table, - Domain, -} - -#[derive(DeriveIden)] -enum Activities { - Table, - Actor, - Object, - Published, -} - -#[derive(DeriveIden)] -enum Objects { - Table, - AttributedTo, -} diff --git a/src/migrations/m20240323_000001_add_user_configs.rs b/src/migrations/m20240323_000001_add_user_configs.rs deleted file mode 100644 index 42dc8fd..0000000 --- a/src/migrations/m20240323_000001_add_user_configs.rs +++ /dev/null @@ -1,49 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Configs::Table) - .col( - ColumnDef::new(Configs::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Configs::AcceptFollowRequests).boolean().not_null()) - .col(ColumnDef::new(Configs::ShowFollowersCount).boolean().not_null()) - .col(ColumnDef::new(Configs::ShowFollowingCount).boolean().not_null()) - .col(ColumnDef::new(Configs::ShowFollowers).boolean().not_null()) - .col(ColumnDef::new(Configs::ShowFollowing).boolean().not_null()) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Configs::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Configs { - Table, - Id, - AcceptFollowRequests, - ShowFollowersCount, - ShowFollowingCount, - ShowFollowers, - ShowFollowing, -} diff --git a/src/migrations/m20240323_000002_add_simple_credentials.rs b/src/migrations/m20240323_000002_add_simple_credentials.rs deleted file mode 100644 index 4600716..0000000 --- a/src/migrations/m20240323_000002_add_simple_credentials.rs +++ /dev/null @@ -1,67 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Credentials::Table) - .col( - ColumnDef::new(Credentials::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Credentials::Email).string().not_null()) - .col(ColumnDef::new(Credentials::Password).string().not_null()) - .to_owned() - ) - .await?; - - manager - .create_table( - Table::create() - .table(Sessions::Table) - .col( - ColumnDef::new(Sessions::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Sessions::Actor).string().not_null()) - .col(ColumnDef::new(Sessions::Expires).date_time().not_null()) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Credentials::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Credentials { - Table, - Id, - Email, - Password, -} - -#[derive(DeriveIden)] -enum Sessions { - Table, - Id, // TODO here ID is the session "secret" but in Credentials it's the actor ID (String) ??? weird!! - Actor, - Expires, -} diff --git a/src/migrations/m20240324_000001_add_addressing.rs b/src/migrations/m20240324_000001_add_addressing.rs deleted file mode 100644 index 358a556..0000000 --- a/src/migrations/m20240324_000001_add_addressing.rs +++ /dev/null @@ -1,110 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Addressing::Table) - .col( - ColumnDef::new(Addressing::Id) - .integer() - .not_null() - .auto_increment() - .primary_key() - ) - .col(ColumnDef::new(Addressing::Actor).string().not_null()) - .col(ColumnDef::new(Addressing::Server).string().not_null()) - .col(ColumnDef::new(Addressing::Activity).string().null()) - .col(ColumnDef::new(Addressing::Object).string().null()) - .col(ColumnDef::new(Addressing::Published).date_time().not_null()) - .to_owned() - ) - .await?; - - // TODO these indexes may not be ordered, killing out timeline query performance - // it may be necessary to include datetime in the index itself? or maybe specify - // some ordering to use another type of indes? - - manager - .create_index( - Index::create() - .name("addressing-actor-index") - .table(Addressing::Table) - .col(Addressing::Actor) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-server-index") - .table(Addressing::Table) - .col(Addressing::Server) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-activity-index") - .table(Addressing::Table) - .col(Addressing::Activity) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-object-index") - .table(Addressing::Table) - .col(Addressing::Object) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Addressing::Table).to_owned()) - .await?; - - manager - .drop_index(Index::drop().name("addressing-actor-index").to_owned()) - .await?; - - manager - .drop_index(Index::drop().name("addressing-server-index").to_owned()) - .await?; - - manager - .drop_index(Index::drop().name("addressing-activity-index").to_owned()) - .await?; - - manager - .drop_index(Index::drop().name("addressing-object-index").to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Addressing { - Table, - Id, - Actor, - Server, - Activity, - Object, - Published, -} diff --git a/src/migrations/m20240325_000001_add_deliveries.rs b/src/migrations/m20240325_000001_add_deliveries.rs deleted file mode 100644 index a2b9cc1..0000000 --- a/src/migrations/m20240325_000001_add_deliveries.rs +++ /dev/null @@ -1,66 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Deliveries::Table) - .col( - ColumnDef::new(Deliveries::Id) - .integer() - .not_null() - .auto_increment() - .primary_key() - ) - .col(ColumnDef::new(Deliveries::Actor).string().not_null()) - .col(ColumnDef::new(Deliveries::Target).string().not_null()) - .col(ColumnDef::new(Deliveries::Activity).string().not_null()) - .col(ColumnDef::new(Deliveries::Created).date_time().not_null()) - .col(ColumnDef::new(Deliveries::NotBefore).date_time().not_null()) - .col(ColumnDef::new(Deliveries::Attempt).integer().not_null()) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("deliveries-notbefore-index") - .table(Deliveries::Table) - .col((Deliveries::NotBefore, IndexOrder::Asc)) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Deliveries::Table).to_owned()) - .await?; - - manager - .drop_index(Index::drop().name("deliveries-notbefore-index").to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Deliveries { - Table, - Id, - Actor, - Target, - Activity, - Created, - NotBefore, - Attempt, -} diff --git a/src/migrations/m20240325_000002_add_system_key.rs b/src/migrations/m20240325_000002_add_system_key.rs deleted file mode 100644 index dbd1954..0000000 --- a/src/migrations/m20240325_000002_add_system_key.rs +++ /dev/null @@ -1,46 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Application::Table) - .col( - ColumnDef::new(Application::Id) - .integer() - .not_null() - .auto_increment() - .primary_key() - ) - .col(ColumnDef::new(Application::PrivateKey).string().not_null()) - .col(ColumnDef::new(Application::PublicKey).string().not_null()) - .col(ColumnDef::new(Application::Created).date_time().not_null()) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Application::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Application { - Table, - Id, - PrivateKey, - PublicKey, - Created, -} diff --git a/src/migrations/m20240418_000001_add_statuses_and_reply_to.rs b/src/migrations/m20240418_000001_add_statuses_and_reply_to.rs deleted file mode 100644 index effb34f..0000000 --- a/src/migrations/m20240418_000001_add_statuses_and_reply_to.rs +++ /dev/null @@ -1,72 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Users::Table) - .add_column( - ColumnDef::new(Users::StatusesCount) - .integer() - .not_null() - .default(0) - ) - .to_owned() - ) - .await?; - - manager - .alter_table( - Table::alter() - .table(Objects::Table) - .add_column( - ColumnDef::new(Objects::InReplyTo) - .string() - .null() - ) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Users::Table) - .drop_column(Users::StatusesCount) - .to_owned() - ) - .await?; - - manager - .alter_table( - Table::alter() - .table(Objects::Table) - .drop_column(Objects::InReplyTo) - .to_owned() - ) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Users { - Table, - StatusesCount, -} - -#[derive(DeriveIden)] -enum Objects { - Table, - InReplyTo, -} diff --git a/src/migrations/m20240421_000001_add_attachments.rs b/src/migrations/m20240421_000001_add_attachments.rs deleted file mode 100644 index dd66d90..0000000 --- a/src/migrations/m20240421_000001_add_attachments.rs +++ /dev/null @@ -1,66 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Attachments::Table) - .col( - ColumnDef::new(Attachments::Id) - .integer() - .not_null() - .auto_increment() - .primary_key() - ) - .col(ColumnDef::new(Attachments::Url).string().not_null()) - .col(ColumnDef::new(Attachments::Object).string().not_null()) - .col(ColumnDef::new(Attachments::DocumentType).string().not_null()) - .col(ColumnDef::new(Attachments::Name).string().null()) - .col(ColumnDef::new(Attachments::MediaType).string().not_null()) - .col(ColumnDef::new(Attachments::Created).date_time().not_null()) - .to_owned() - ) - .await?; - - manager - .create_index( - Index::create() - .name("attachment-object-index") - .table(Attachments::Table) - .col(Attachments::Object) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Attachments::Table).to_owned()) - .await?; - - manager - .drop_index(Index::drop().name("attachment-object-index").to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Attachments { - Table, - Id, - Url, - Object, - DocumentType, - Name, - MediaType, - Created, -} diff --git a/src/migrations/m20240424_000001_add_sensitive_field.rs b/src/migrations/m20240424_000001_add_sensitive_field.rs deleted file mode 100644 index 1ec8fbd..0000000 --- a/src/migrations/m20240424_000001_add_sensitive_field.rs +++ /dev/null @@ -1,45 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Objects::Table) - .add_column( - ColumnDef::new(Objects::Sensitive) - .boolean() - .not_null() - .default(false) - ) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Objects::Table) - .drop_column(Objects::Sensitive) - .to_owned() - ) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Objects { - Table, - Sensitive, -} - diff --git a/src/migrations/m20240429_000001_add_relays_table.rs b/src/migrations/m20240429_000001_add_relays_table.rs deleted file mode 100644 index 21b4141..0000000 --- a/src/migrations/m20240429_000001_add_relays_table.rs +++ /dev/null @@ -1,43 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Relays::Table) - .col( - ColumnDef::new(Relays::Id) - .string() - .not_null() - .primary_key() - ) - .col(ColumnDef::new(Relays::Accepted).boolean().not_null().default(false)) - .col(ColumnDef::new(Relays::Forwarding).boolean().not_null().default(false)) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Relays::Table).to_owned()) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Relays { - Table, - Id, - Accepted, - Forwarding, -} diff --git a/src/migrations/m20240502_000001_add_object_updated.rs b/src/migrations/m20240502_000001_add_object_updated.rs deleted file mode 100644 index 863c5e0..0000000 --- a/src/migrations/m20240502_000001_add_object_updated.rs +++ /dev/null @@ -1,40 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Objects::Table) - .add_column(ColumnDef::new(Objects::Updated).date_time().null()) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Objects::Table) - .drop_column(Objects::Updated) - .to_owned() - ) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Objects { - Table, - Updated, -} - diff --git a/src/migrations/m20240520_000001_add_published_to_addressing_actor_index.rs b/src/migrations/m20240520_000001_add_published_to_addressing_actor_index.rs deleted file mode 100644 index 477dbe7..0000000 --- a/src/migrations/m20240520_000001_add_published_to_addressing_actor_index.rs +++ /dev/null @@ -1,82 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - - manager - .drop_index(Index::drop().name("addressing-actor-index").to_owned()) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-actor-published-index") - .table(Addressing::Table) - .col(Addressing::Actor) - .col(Addressing::Published) - .to_owned() - ) - .await?; - - manager - .drop_index(Index::drop().name("addressing-server-index").to_owned()) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-server-published-index") - .table(Addressing::Table) - .col(Addressing::Server) - .col(Addressing::Published) - .to_owned() - ) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_index(Index::drop().name("addressing-actor-published-index").to_owned()) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-actor-index") - .table(Addressing::Table) - .col(Addressing::Actor) - .to_owned() - ) - .await?; - - manager - .drop_index(Index::drop().name("addressing-server-published-index").to_owned()) - .await?; - - manager - .create_index( - Index::create() - .name("addressing-server-index") - .table(Addressing::Table) - .col(Addressing::Server) - .to_owned() - ) - .await?; - - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Addressing { - Table, - Actor, - Server, - Published, -} diff --git a/src/migrations/m20240524_000001_create_actor_activity_object_tables.rs b/src/migrations/m20240524_000001_create_actor_activity_object_tables.rs new file mode 100644 index 0000000..b5721bb --- /dev/null +++ b/src/migrations/m20240524_000001_create_actor_activity_object_tables.rs @@ -0,0 +1,392 @@ +use sea_orm_migration::prelude::*; +#[derive(DeriveIden)] +pub enum Actors { + Table, + Internal, + Id, + Domain, + ActorType, + Name, + Summary, + Image, + Icon, + PreferredUsername, + Inbox, + SharedInbox, + Outbox, + Following, + FollowingCount, + Followers, + FollowersCount, + StatusesCount, + PublicKey, + PrivateKey, + Published, + Updated, +} + +#[derive(DeriveIden)] +pub enum Activities { + Table, + Internal, + Id, + ActivityType, + Actor, + Object, + Target, + Cc, + Bcc, + To, + Bto, + Published, +} + +#[derive(DeriveIden)] +pub enum Objects { + Table, + Internal, + Id, + ObjectType, + AttributedTo, + Name, + Summary, + Content, + Sensitive, + Url, + Likes, + Announces, + Replies, + Context, + InReplyTo, + Cc, + Bcc, + To, + Bto, + Published, + Updated, +} + +#[derive(DeriveIden)] +pub enum Instances { + Table, + Internal, + Domain, + Name, + Software, + Version, + Icon, + DownSince, + Users, + Posts, + Published, + Updated, +} + + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + + manager + .create_table( + Table::create() + .table(Instances::Table) + .comment("known other instances in the fediverse") + .col( + ColumnDef::new(Instances::Internal) + .big_integer() + .not_null() + .auto_increment() + .primary_key() + ) + .col(ColumnDef::new(Instances::Domain).string().not_null().unique_key()) + .col(ColumnDef::new(Instances::Name).string().null()) + .col(ColumnDef::new(Instances::Software).string().null()) + .col(ColumnDef::new(Instances::Version).string().null()) + .col(ColumnDef::new(Instances::Icon).string().null()) + .col(ColumnDef::new(Instances::DownSince).date_time().null()) + .col(ColumnDef::new(Instances::Users).big_integer().null()) + .col(ColumnDef::new(Instances::Posts).big_integer().null()) + .col(ColumnDef::new(Instances::Published).date_time().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Instances::Updated).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().unique().name("index-instances-domain").table(Instances::Table).col(Instances::Domain).to_owned()) + .await?; + + + + manager + .create_table( + Table::create() + .table(Actors::Table) + .comment("main actors table, with users and applications") + .col( + ColumnDef::new(Actors::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Actors::Id).string().not_null().unique_key()) + .col(ColumnDef::new(Actors::ActorType).string().not_null()) + .col(ColumnDef::new(Actors::Domain).string().not_null()) + // .foreign_key( + // ForeignKey::create() + // .name("fkey-actors-instances") + // .from(Actors::Table, Actors::Domain) + // .to(Instances::Table, Instances::Domain) + // .on_update(ForeignKeyAction::Cascade) + // ) + .col(ColumnDef::new(Actors::Name).string().null()) + .col(ColumnDef::new(Actors::Summary).string().null()) + .col(ColumnDef::new(Actors::Image).string().null()) + .col(ColumnDef::new(Actors::Icon).string().null()) + .col(ColumnDef::new(Actors::PreferredUsername).string().not_null()) + .col(ColumnDef::new(Actors::Inbox).string().null()) + .col(ColumnDef::new(Actors::SharedInbox).string().null()) + .col(ColumnDef::new(Actors::Outbox).string().null()) + .col(ColumnDef::new(Actors::Following).string().null()) + .col(ColumnDef::new(Actors::Followers).string().null()) + .col(ColumnDef::new(Actors::FollowingCount).integer().not_null().default(0)) + .col(ColumnDef::new(Actors::FollowersCount).integer().not_null().default(0)) + .col(ColumnDef::new(Actors::StatusesCount).integer().not_null().default(0)) + .col(ColumnDef::new(Actors::PublicKey).string().not_null()) + .col(ColumnDef::new(Actors::PrivateKey).string().null()) + .col(ColumnDef::new(Actors::Published).date_time().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Actors::Updated).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().unique().name("index-actors-id").table(Actors::Table).col(Actors::Id).to_owned()) + .await?; + + manager + .create_index( + Index::create() + .unique() + .name("index-actors-preferred-username-domain") + .table(Actors::Table) + .col(Actors::PreferredUsername) + .col(Actors::Domain) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-actors-domain").table(Actors::Table).col(Actors::Domain).to_owned()) + .await?; + + + + manager + .create_table( + Table::create() + .table(Objects::Table) + .comment("objects are all AP documents which are neither actors nor activities") + .col( + ColumnDef::new(Objects::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Objects::Id).string().not_null().unique_key()) + .col(ColumnDef::new(Objects::ObjectType).string().not_null()) + .col(ColumnDef::new(Objects::AttributedTo).string().null()) + // .foreign_key( + // ForeignKey::create() + // .name("fkey-objects-attributed-to") + // .from(Objects::Table, Objects::AttributedTo) + // .to(Actors::Table, Actors::Internal) + // .on_update(ForeignKeyAction::Cascade) + // ) + .col(ColumnDef::new(Objects::Name).string().null()) + .col(ColumnDef::new(Objects::Summary).string().null()) + .col(ColumnDef::new(Objects::Content).string().null()) + .col(ColumnDef::new(Objects::Sensitive).boolean().not_null().default(false)) + .col(ColumnDef::new(Objects::InReplyTo).string().null()) + // .foreign_key( + // ForeignKey::create() + // .name("fkey-objects-in-reply-to") + // .from(Objects::Table, Objects::InReplyTo) + // .to(Objects::Table, Objects::Id) + // .on_update(ForeignKeyAction::Cascade) + // ) + .col(ColumnDef::new(Objects::Url).string().null()) + .col(ColumnDef::new(Objects::Likes).integer().not_null().default(0)) + .col(ColumnDef::new(Objects::Announces).integer().not_null().default(0)) + .col(ColumnDef::new(Objects::Replies).integer().not_null().default(0)) + .col(ColumnDef::new(Objects::Context).string().null()) + .col(ColumnDef::new(Objects::To).json().null()) + .col(ColumnDef::new(Objects::Bto).json().null()) + .col(ColumnDef::new(Objects::Cc).json().null()) + .col(ColumnDef::new(Objects::Bcc).json().null()) + .col(ColumnDef::new(Objects::Published).date_time().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Objects::Updated).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ).await?; + + manager + .create_index(Index::create().unique().name("index-objects-id").table(Objects::Table).col(Objects::Id).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-objects-attributed-to").table(Objects::Table).col(Objects::AttributedTo).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-objects-in-reply-to").table(Objects::Table).col(Objects::InReplyTo).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-objects-content-text").table(Objects::Table).col(Objects::Content).full_text().to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-objects-context").table(Objects::Table).col(Objects::Context).to_owned()) + .await?; + + + + manager + .create_table( + Table::create() + .table(Activities::Table) + .comment("all activities this instance ever received or generated") + .col( + ColumnDef::new(Activities::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Activities::Id).string().not_null().unique_key()) + .col(ColumnDef::new(Activities::ActivityType).string().not_null()) + .col(ColumnDef::new(Activities::Actor).string().not_null()) + // .foreign_key( + // ForeignKey::create() + // .name("fkey-activities-actors") + // .from(Activities::Table, Activities::Actor) + // .to(Actors::Table, Actors::Id) + // .on_update(ForeignKeyAction::Cascade) + // ) + .col(ColumnDef::new(Activities::Object).string().null()) + // .foreign_key( + // ForeignKey::create() + // .name("fkey-activities-objects") + // .from(Activities::Table, Activities::Object) + // .to(Objects::Table, Objects::Internal) + // .on_update(ForeignKeyAction::Cascade) + // ) + .col(ColumnDef::new(Activities::Target).string().null()) + .col(ColumnDef::new(Activities::To).json().null()) + .col(ColumnDef::new(Activities::Bto).json().null()) + .col(ColumnDef::new(Activities::Cc).json().null()) + .col(ColumnDef::new(Activities::Bcc).json().null()) + .col(ColumnDef::new(Activities::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ).await?; + + manager + .create_index(Index::create().unique().name("index-activities-id").table(Activities::Table).col(Activities::Id).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-activities-actor").table(Activities::Table).col(Activities::Actor).to_owned()) + .await?; + + manager + .create_index(Index::create().name("activities-object-index").table(Activities::Table).col(Activities::Object).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-activities-published-descending").table(Activities::Table).col((Activities::Published, IndexOrder::Desc)).to_owned()) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Actors::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-actors-id").table(Actors::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-actors-preferred-username").table(Actors::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-actors-domain").table(Actors::Table).to_owned()) + .await?; + + + manager + .drop_table(Table::drop().table(Activities::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-activities-id").table(Activities::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-activities-actor").table(Activities::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("activities-object-index").table(Activities::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-activities-published-descending").table(Activities::Table).to_owned()) + .await?; + + + manager + .drop_table(Table::drop().table(Objects::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-objects-id").table(Objects::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-objects-attributed-to").table(Objects::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-objects-in-reply-to").table(Objects::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-objects-content-text").table(Objects::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-objects-context").table(Objects::Table).to_owned()) + .await?; + + + manager + .drop_table(Table::drop().table(Instances::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-instances-domain").table(Instances::Table).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/src/migrations/m20240524_000002_create_relations_likes_shares.rs b/src/migrations/m20240524_000002_create_relations_likes_shares.rs new file mode 100644 index 0000000..4a13966 --- /dev/null +++ b/src/migrations/m20240524_000002_create_relations_likes_shares.rs @@ -0,0 +1,257 @@ +use sea_orm_migration::prelude::*; + +use super::m20240524_000001_create_actor_activity_object_tables::{Activities, Actors, Objects}; + +#[derive(DeriveIden)] +pub enum Relations { + Table, + Internal, + Follower, + Following, + Activity, + Accept, +} + +#[derive(DeriveIden)] +#[allow(clippy::enum_variant_names)] +pub enum Likes { + Table, + Internal, + Actor, + Object, + Activity, + Published, +} + +#[derive(DeriveIden)] +#[allow(clippy::enum_variant_names)] +pub enum Announces { + Table, + Internal, + Actor, + Object, + Published, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Relations::Table) + .comment("follow relations between actors (applications too! for relays)") + .col( + ColumnDef::new(Relations::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Relations::Follower).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-relations-follower") + .from(Relations::Table, Relations::Follower) + .to(Actors::Table, Actors::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Relations::Following).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-relations-following") + .from(Relations::Table, Relations::Following) + .to(Actors::Table, Actors::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Relations::Accept).big_integer().null()) + .foreign_key( + ForeignKey::create() + .name("fkey-relations-accept") + .from(Relations::Table, Relations::Accept) + .to(Activities::Table, Activities::Internal) + .on_update(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Relations::Activity).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-relations-activity") + .from(Relations::Table, Relations::Activity) + .to(Activities::Table, Activities::Internal) + .on_update(ForeignKeyAction::Cascade) + ) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-relations-follower").table(Relations::Table).col(Relations::Follower).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-relations-following").table(Relations::Table).col(Relations::Following).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-relations-activity").table(Relations::Table).col(Relations::Activity).to_owned()) + .await?; + + manager + .create_table( + Table::create() + .table(Likes::Table) + .comment("all like events, joining actor to object") + .col( + ColumnDef::new(Likes::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Likes::Actor).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-likes-actor") + .from(Likes::Table, Likes::Actor) + .to(Actors::Table, Actors::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Likes::Object).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-likes-object") + .from(Likes::Table, Likes::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Likes::Activity).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-likes-activity") + .from(Likes::Table, Likes::Activity) + .to(Activities::Table, Activities::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Likes::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-likes-actor").table(Likes::Table).col(Likes::Actor).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-likes-object").table(Likes::Table).col(Likes::Object).to_owned()) + .await?; + + manager + .create_index( + Index::create() + .unique() + .name("index-likes-actor-object") + .table(Likes::Table) + .col(Likes::Actor) + .col(Likes::Object) + .to_owned() + ).await?; + + manager + .create_table( + Table::create() + .table(Announces::Table) + .comment("all share/boost/reblog events, joining actor to object") + .col( + ColumnDef::new(Announces::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Announces::Actor).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-announces-actor") + .from(Announces::Table, Announces::Actor) + .to(Actors::Table, Actors::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Announces::Object).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-announces-object") + .from(Announces::Table, Announces::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Announces::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-announces-actor").table(Announces::Table).col(Announces::Actor).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-announces-object").table(Announces::Table).col(Announces::Object).to_owned()) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Relations::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-relations-follower").table(Relations::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-relations-following").table(Relations::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-relations-activity").table(Relations::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Likes::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-likes-actor").table(Likes::Table).to_owned()) + .await?; + manager + .drop_index(Index::drop().name("index-likes-object").table(Likes::Table).to_owned()) + .await?; + manager + .drop_index(Index::drop().name("index-likes-actor-object").table(Likes::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Announces::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-announces-actor").table(Announces::Table).to_owned()) + .await?; + manager + .drop_index(Index::drop().name("index-announces-object").table(Announces::Table).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/src/migrations/m20240524_000003_create_users_auth_and_config.rs b/src/migrations/m20240524_000003_create_users_auth_and_config.rs new file mode 100644 index 0000000..aeb4834 --- /dev/null +++ b/src/migrations/m20240524_000003_create_users_auth_and_config.rs @@ -0,0 +1,171 @@ +use sea_orm_migration::prelude::*; + +use super::m20240524_000001_create_actor_activity_object_tables::Actors; + +#[derive(DeriveIden)] +pub enum Configs { + Table, + Internal, + Actor, + AcceptFollowRequests, + ShowFollowersCount, + ShowFollowingCount, + ShowFollowers, + ShowFollowing, +} + +#[derive(DeriveIden)] +pub enum Credentials { + Table, + Internal, + Actor, + Login, + Password, +} + +#[derive(DeriveIden)] +pub enum Sessions { + Table, + Internal, + Actor, + Secret, + Expires, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Configs::Table) + .comment("configuration for each local user") + .col( + ColumnDef::new(Configs::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Configs::Actor).string().not_null().unique_key()) + .foreign_key( + ForeignKey::create() + .name("fkey-config-actor") + .from(Configs::Table, Configs::Actor) + .to(Actors::Table, Actors::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Configs::AcceptFollowRequests).boolean().not_null()) + .col(ColumnDef::new(Configs::ShowFollowersCount).boolean().not_null()) + .col(ColumnDef::new(Configs::ShowFollowingCount).boolean().not_null()) + .col(ColumnDef::new(Configs::ShowFollowers).boolean().not_null()) + .col(ColumnDef::new(Configs::ShowFollowing).boolean().not_null()) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().unique().name("index-configs-actor").table(Configs::Table).col(Configs::Actor).to_owned()) + .await?; + + manager + .create_table( + Table::create() + .table(Credentials::Table) + .comment("simple login credentials to authenticate local users") + .col( + ColumnDef::new(Credentials::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Credentials::Actor).string().not_null().unique_key()) + .foreign_key( + ForeignKey::create() + .name("fkey-credentials-actor") + .from(Credentials::Table, Credentials::Actor) + .to(Actors::Table, Actors::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Credentials::Login).string().not_null()) + .col(ColumnDef::new(Credentials::Password).string().not_null()) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().unique().name("index-credentials-actor").table(Credentials::Table).col(Credentials::Actor).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-credentials-login").table(Credentials::Table).col(Credentials::Login).to_owned()) + .await?; + + manager + .create_table( + Table::create() + .table(Sessions::Table) + .comment("authenticated sessions from local users") + .col( + ColumnDef::new(Sessions::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Sessions::Actor).string().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-sessions-actor") + .from(Sessions::Table, Sessions::Actor) + .to(Actors::Table, Actors::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Sessions::Secret).string().not_null()) + .col(ColumnDef::new(Sessions::Expires).date_time().not_null()) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-sessions-secret").table(Sessions::Table).col(Sessions::Secret).to_owned()) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Configs::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-configs-actor").table(Configs::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Credentials::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-credentials-login").table(Credentials::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Sessions::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-sessions-secret").table(Sessions::Table).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/src/migrations/m20240524_000004_create_addressing_deliveries.rs b/src/migrations/m20240524_000004_create_addressing_deliveries.rs new file mode 100644 index 0000000..50bfc0d --- /dev/null +++ b/src/migrations/m20240524_000004_create_addressing_deliveries.rs @@ -0,0 +1,195 @@ +use sea_orm_migration::prelude::*; + +use super::m20240524_000001_create_actor_activity_object_tables::{Activities, Actors, Instances, Objects}; + +#[derive(DeriveIden)] +pub enum Addressing { + Table, + Internal, + Actor, + Instance, + Activity, + Object, + Published, +} + +#[derive(DeriveIden)] +pub enum Deliveries { + Table, + Internal, + Actor, + Target, + Activity, + Published, + NotBefore, + Attempt, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Addressing::Table) + .comment("this join table contains all addressing relations, serving effectively as permissions truth table") + .col( + ColumnDef::new(Addressing::Internal) + .big_integer() + .not_null() + .auto_increment() + .primary_key() + ) + .col(ColumnDef::new(Addressing::Actor).big_integer().null()) + .foreign_key( + ForeignKey::create() + .name("fkey-addressing-actor") + .from(Addressing::Table, Addressing::Actor) + .to(Actors::Table, Actors::Internal) + .on_update(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Addressing::Instance).big_integer().null()) + .foreign_key( + ForeignKey::create() + .name("fkey-addressing-instance") + .from(Addressing::Table, Addressing::Instance) + .to(Instances::Table, Instances::Internal) + .on_update(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Addressing::Activity).big_integer().null()) + .foreign_key( + ForeignKey::create() + .name("fkey-addressing-activity") + .from(Addressing::Table, Addressing::Activity) + .to(Activities::Table, Activities::Internal) + .on_update(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Addressing::Object).big_integer().null()) + .foreign_key( + ForeignKey::create() + .name("fkey-addressing-object") + .from(Addressing::Table, Addressing::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Addressing::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index( + Index::create() + .name("index-addressing-actor-published") + .table(Addressing::Table) + .col(Addressing::Actor) + .col(Addressing::Published) + .to_owned() + ) + .await?; + + manager + .create_index( + Index::create() + .name("index-addressing-instance-published") + .table(Addressing::Table) + .col(Addressing::Instance) + .col(Addressing::Published) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-addressing-activity").table(Addressing::Table).col(Addressing::Activity).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-addressing-object").table(Addressing::Table).col(Addressing::Object).to_owned()) + .await?; + + manager + .create_table( + Table::create() + .table(Deliveries::Table) + .comment("this table contains all enqueued outgoing delivery jobs") + .col( + ColumnDef::new(Deliveries::Internal) + .big_integer() + .not_null() + .auto_increment() + .primary_key() + ) + .col(ColumnDef::new(Deliveries::Actor).string().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-deliveries-actor") + .from(Deliveries::Table, Deliveries::Actor) + .to(Actors::Table, Actors::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Deliveries::Target).string().not_null()) + .col(ColumnDef::new(Deliveries::Activity).string().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-deliveries-activity") + .from(Deliveries::Table, Deliveries::Activity) + .to(Activities::Table, Activities::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Deliveries::Published).date_time().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Deliveries::NotBefore).date_time().not_null().default(Expr::current_timestamp())) + .col(ColumnDef::new(Deliveries::Attempt).integer().not_null().default(0)) + .to_owned() + ) + .await?; + + manager + .create_index( + Index::create() + .name("index-deliveries-not-before") + .table(Deliveries::Table) + .col((Deliveries::NotBefore, IndexOrder::Asc)) + .to_owned() + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Addressing::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-addressing-actor").to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-addressing-server").to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-addressing-activity").to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-addressing-object").to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Deliveries::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-deliveries-not-before").to_owned()) + .await?; + + Ok(()) + } +} diff --git a/src/migrations/m20240524_000005_create_attachments_tags_mentions.rs b/src/migrations/m20240524_000005_create_attachments_tags_mentions.rs new file mode 100644 index 0000000..863bcf5 --- /dev/null +++ b/src/migrations/m20240524_000005_create_attachments_tags_mentions.rs @@ -0,0 +1,207 @@ +use sea_orm_migration::prelude::*; + +use super::m20240524_000001_create_actor_activity_object_tables::Objects; + +#[derive(DeriveIden)] +pub enum Attachments { + Table, + Internal, + DocumentType, + Url, + Object, + Name, + MediaType, + Published, +} + +#[derive(DeriveIden)] +pub enum Mentions { + Table, + Internal, + Object, + Actor, + Published, +} + +#[derive(DeriveIden)] +pub enum Hashtags { + Table, + Internal, + Object, + Name, + Published, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Attachments::Table) + .comment("media attachments related to objects") + .col( + ColumnDef::new(Attachments::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Attachments::Url).string().not_null().unique_key()) + .col(ColumnDef::new(Attachments::Object).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-attachments-object") + .from(Attachments::Table, Attachments::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Attachments::DocumentType).string().not_null()) + .col(ColumnDef::new(Attachments::Name).string().null()) + .col(ColumnDef::new(Attachments::MediaType).string().not_null()) + .col(ColumnDef::new(Attachments::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-attachment-object").table(Attachments::Table).col(Attachments::Object).to_owned()) + .await?; + + manager + .create_table( + Table::create() + .table(Mentions::Table) + .comment("join table relating posts to mentioned users") + .col( + ColumnDef::new(Mentions::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Mentions::Object).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-mentions-object") + .from(Mentions::Table, Mentions::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Mentions::Actor).string().not_null()) + // .foreign_key( + // ForeignKey::create() + // .name("fkey-mentions-actor") + // .from(Mentions::Table, Mentions::Actor) + // .to(Actors::Table, Actors::Internal) + // .on_update(ForeignKeyAction::Cascade) + // .on_delete(ForeignKeyAction::Cascade) + // ) + .col(ColumnDef::new(Mentions::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-mentions-object").table(Mentions::Table).col(Mentions::Object).to_owned()) + .await?; + + manager + .create_index( + Index::create() + .name("index-mentions-actor-published") + .table(Mentions::Table) + .col(Mentions::Actor) + .col((Mentions::Published, IndexOrder::Desc)) + .to_owned() + ) + .await?; + + manager + .create_table( + Table::create() + .table(Hashtags::Table) + .comment("join table relating posts to hashtags") + .col( + ColumnDef::new(Hashtags::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Hashtags::Object).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-hashtags-object") + .from(Hashtags::Table, Hashtags::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Hashtags::Name).string().not_null()) + .col(ColumnDef::new(Hashtags::Published).date_time().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-hashtags-object").table(Hashtags::Table).col(Hashtags::Object).to_owned()) + .await?; + + manager + .create_index( + Index::create() + .name("index-hashtags-name-published") + .table(Hashtags::Table) + .col(Hashtags::Name) + .col((Hashtags::Published, IndexOrder::Desc)) + .to_owned() + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Attachments::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-attachment-object").to_owned()) + .await?; + + + manager + .drop_table(Table::drop().table(Mentions::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-mentions-object").table(Mentions::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-mentions-actor-published").table(Mentions::Table).to_owned()) + .await?; + + + manager + .drop_table(Table::drop().table(Hashtags::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-hashtags-object").table(Hashtags::Table).to_owned()) + .await?; + + manager + .drop_index(Index::drop().name("index-hashtags-name-published").table(Hashtags::Table).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/src/migrations/m20240512_000001_add_url_to_objects.rs b/src/migrations/m20240529_000001_add_relation_unique_index.rs similarity index 50% rename from src/migrations/m20240512_000001_add_url_to_objects.rs rename to src/migrations/m20240529_000001_add_relation_unique_index.rs index 373549a..c3c253f 100644 --- a/src/migrations/m20240512_000001_add_url_to_objects.rs +++ b/src/migrations/m20240529_000001_add_relation_unique_index.rs @@ -1,5 +1,7 @@ use sea_orm_migration::prelude::*; +use super::m20240524_000002_create_relations_likes_shares::Relations; + #[derive(DeriveMigrationName)] pub struct Migration; @@ -7,34 +9,23 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .alter_table( - Table::alter() - .table(Objects::Table) - .add_column(ColumnDef::new(Objects::Url).string().null()) + .create_index( + Index::create() + .unique() + .name("index-relations-follower-following") + .table(Relations::Table) + .col(Relations::Following) + .col(Relations::Follower) .to_owned() - ) + ) .await?; - Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .alter_table( - Table::alter() - .table(Objects::Table) - .drop_column(Objects::Url) - .to_owned() - ) + .drop_index(Index::drop().name("index-relations-follower-following").table(Relations::Table).to_owned()) .await?; - Ok(()) } } - -#[derive(DeriveIden)] -enum Objects { - Table, - Url, -} - diff --git a/src/migrations/mod.rs b/src/migrations/mod.rs index 392d0fb..a8831e1 100644 --- a/src/migrations/mod.rs +++ b/src/migrations/mod.rs @@ -1,21 +1,11 @@ use sea_orm_migration::prelude::*; -mod m20240316_000001_create_table; -mod m20240322_000001_create_relations; -mod m20240322_000002_add_likes_shares; -mod m20240322_000003_add_indexes; -mod m20240323_000001_add_user_configs; -mod m20240323_000002_add_simple_credentials; -mod m20240324_000001_add_addressing; -mod m20240325_000001_add_deliveries; -mod m20240325_000002_add_system_key; -mod m20240418_000001_add_statuses_and_reply_to; -mod m20240421_000001_add_attachments; -mod m20240424_000001_add_sensitive_field; -mod m20240429_000001_add_relays_table; -mod m20240502_000001_add_object_updated; -mod m20240512_000001_add_url_to_objects; -mod m20240520_000001_add_published_to_addressing_actor_index; +mod m20240524_000001_create_actor_activity_object_tables; +mod m20240524_000002_create_relations_likes_shares; +mod m20240524_000003_create_users_auth_and_config; +mod m20240524_000004_create_addressing_deliveries; +mod m20240524_000005_create_attachments_tags_mentions; +mod m20240529_000001_add_relation_unique_index; pub struct Migrator; @@ -23,22 +13,12 @@ pub struct Migrator; impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ - Box::new(m20240316_000001_create_table::Migration), - Box::new(m20240322_000001_create_relations::Migration), - Box::new(m20240322_000002_add_likes_shares::Migration), - Box::new(m20240322_000003_add_indexes::Migration), - Box::new(m20240323_000001_add_user_configs::Migration), - Box::new(m20240323_000002_add_simple_credentials::Migration), - Box::new(m20240324_000001_add_addressing::Migration), - Box::new(m20240325_000001_add_deliveries::Migration), - Box::new(m20240325_000002_add_system_key::Migration), - Box::new(m20240418_000001_add_statuses_and_reply_to::Migration), - Box::new(m20240421_000001_add_attachments::Migration), - Box::new(m20240424_000001_add_sensitive_field::Migration), - Box::new(m20240429_000001_add_relays_table::Migration), - Box::new(m20240502_000001_add_object_updated::Migration), - Box::new(m20240512_000001_add_url_to_objects::Migration), - Box::new(m20240520_000001_add_published_to_addressing_actor_index::Migration), + Box::new(m20240524_000001_create_actor_activity_object_tables::Migration), + Box::new(m20240524_000002_create_relations_likes_shares::Migration), + Box::new(m20240524_000003_create_users_auth_and_config::Migration), + Box::new(m20240524_000004_create_addressing_deliveries::Migration), + Box::new(m20240524_000005_create_attachments_tags_mentions::Migration), + Box::new(m20240529_000001_add_relation_unique_index::Migration), ] } } diff --git a/src/model/activity.rs b/src/model/activity.rs index 3b48cef..44d5aa7 100644 --- a/src/model/activity.rs +++ b/src/model/activity.rs @@ -1,46 +1,113 @@ -use apb::{ActivityMut, BaseMut, ObjectMut}; -use sea_orm::entity::prelude::*; +use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut}; +use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; -use crate::routes::activitypub::jsonld::LD; - -use super::Audience; +use crate::{model::Audience, errors::UpubError, routes::activitypub::jsonld::LD}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "activities")] pub struct Model { #[sea_orm(primary_key)] + pub internal: i64, + #[sea_orm(unique)] pub id: String, - - pub activity_type: apb::ActivityType, + pub activity_type: ActivityType, pub actor: String, pub object: Option, - - pub target: Option, // TODO relates to USER maybe?? - pub cc: Audience, - pub bcc: Audience, + pub target: Option, pub to: Audience, pub bto: Audience, + pub cc: Audience, + pub bcc: Audience, pub published: ChronoDateTimeUtc, +} - // TODO: origin, result, instrument +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "NoAction" + )] + Actors, + #[sea_orm(has_many = "super::addressing::Entity")] + Addressing, + #[sea_orm(has_many = "super::delivery::Entity")] + Deliveries, + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Id", + on_update = "Cascade", + on_delete = "NoAction" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Addressing.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Deliveries.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_ap_id(id: &str) -> Select { + Entity::find().filter(Column::Id.eq(id)) + } + + pub async fn ap_to_internal(id: &str, db: &DatabaseConnection) -> crate::Result { + Entity::find() + .filter(Column::Id.eq(id)) + .select_only() + .select_column(Column::Internal) + .into_tuple::() + .one(db) + .await? + .ok_or_else(UpubError::not_found) + } +} + +impl ActiveModel { + //#[deprecated = "should remove this, get models thru normalizer"] + pub fn new(activity: &impl apb::Activity) -> Result { + Ok(ActiveModel { + internal: sea_orm::ActiveValue::NotSet, + id: sea_orm::ActiveValue::Set(activity.id().ok_or(super::FieldError("id"))?.to_string()), + activity_type: sea_orm::ActiveValue::Set(activity.activity_type().ok_or(super::FieldError("type"))?), + actor: sea_orm::ActiveValue::Set(activity.actor().id().ok_or(super::FieldError("actor"))?), + object: sea_orm::ActiveValue::Set(activity.object().id()), + target: sea_orm::ActiveValue::Set(activity.target().id()), + published: sea_orm::ActiveValue::Set(activity.published().unwrap_or(chrono::Utc::now())), + to: sea_orm::ActiveValue::Set(activity.to().into()), + bto: sea_orm::ActiveValue::Set(activity.bto().into()), + cc: sea_orm::ActiveValue::Set(activity.cc().into()), + bcc: sea_orm::ActiveValue::Set(activity.bcc().into()), + }) + } } impl Model { - pub fn new(activity: &impl apb::Activity) -> Result { - Ok(Model { - id: activity.id().ok_or(super::FieldError("id"))?.to_string(), - activity_type: activity.activity_type().ok_or(super::FieldError("type"))?, - actor: activity.actor().id().ok_or(super::FieldError("actor"))?, - object: activity.object().id(), - target: activity.target().id(), - published: activity.published().unwrap_or(chrono::Utc::now()), - to: activity.to().into(), - bto: activity.bto().into(), - cc: activity.cc().into(), - bcc: activity.bcc().into(), - }) - } - pub fn ap(self) -> serde_json::Value { serde_json::Value::new_object() .set_id(Some(&self.id)) @@ -56,49 +123,6 @@ impl Model { } } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::Actor", - to = "super::user::Column::Id" - )] - User, - - #[sea_orm( - belongs_to = "super::object::Entity", - from = "Column::Object", - to = "super::object::Column::Id" - )] - Object, - - #[sea_orm(has_many = "super::addressing::Entity")] - Addressing, - - #[sea_orm(has_many = "super::delivery::Entity")] - Delivery, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Object.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Addressing.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - impl apb::target::Addressed for Model { fn addressed(&self) -> Vec { let mut to : Vec = self.to.0.clone(); diff --git a/src/model/actor.rs b/src/model/actor.rs new file mode 100644 index 0000000..92f2ed5 --- /dev/null +++ b/src/model/actor.rs @@ -0,0 +1,242 @@ +use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; + +use apb::{Actor, ActorMut, ActorType, BaseMut, DocumentMut, Endpoints, EndpointsMut, Object, ObjectMut, PublicKey, PublicKeyMut}; + +use crate::{errors::UpubError, routes::activitypub::jsonld::LD}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "actors")] +pub struct Model { + #[sea_orm(primary_key)] + pub internal: i64, + #[sea_orm(unique)] + pub id: String, + pub actor_type: ActorType, + pub domain: String, + pub name: Option, + pub summary: Option, + pub image: Option, + pub icon: Option, + pub preferred_username: String, + pub inbox: Option, + pub shared_inbox: Option, + pub outbox: Option, + pub following: Option, + pub followers: Option, + pub following_count: i32, + pub followers_count: i32, + pub statuses_count: i32, + pub public_key: String, + pub private_key: Option, + pub published: ChronoDateTimeUtc, + pub updated: ChronoDateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::activity::Entity")] + Activities, + #[sea_orm(has_many = "super::addressing::Entity")] + Addressing, + #[sea_orm(has_many = "super::announce::Entity")] + Announces, + #[sea_orm(has_many = "super::config::Entity")] + Configs, + #[sea_orm(has_many = "super::credential::Entity")] + Credentials, + #[sea_orm(has_many = "super::delivery::Entity")] + Deliveries, + #[sea_orm( + belongs_to = "super::instance::Entity", + from = "Column::Domain", + to = "super::instance::Column::Domain", + on_update = "Cascade", + on_delete = "NoAction" + )] + Instances, + #[sea_orm(has_many = "super::like::Entity")] + Likes, + #[sea_orm(has_many = "super::mention::Entity")] + Mentions, + #[sea_orm(has_many = "super::object::Entity")] + Objects, + #[sea_orm(has_many = "super::relation::Entity")] + Relations, + #[sea_orm(has_many = "super::session::Entity")] + Sessions, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Addressing.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Announces.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Configs.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Credentials.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Deliveries.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Instances.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Likes.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Mentions.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Relations.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Sessions.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_ap_id(id: &str) -> Select { + Entity::find().filter(Column::Id.eq(id)) + } + + pub fn delete_by_ap_id(id: &str) -> sea_orm::DeleteMany { + Entity::delete_many().filter(Column::Id.eq(id)) + } + + pub async fn ap_to_internal(id: &str, db: &DatabaseConnection) -> crate::Result { + Entity::find() + .filter(Column::Id.eq(id)) + .select_only() + .select_column(Column::Internal) + .into_tuple::() + .one(db) + .await? + .ok_or_else(UpubError::not_found) + } +} + +impl ActiveModel { + pub fn new(object: &impl Actor) -> Result { + let ap_id = object.id().ok_or(super::FieldError("id"))?.to_string(); + let (domain, fallback_preferred_username) = split_user_id(&ap_id); + Ok(ActiveModel { + internal: sea_orm::ActiveValue::NotSet, + domain: sea_orm::ActiveValue::Set(domain), + id: sea_orm::ActiveValue::Set(ap_id), + preferred_username: sea_orm::ActiveValue::Set(object.preferred_username().unwrap_or(&fallback_preferred_username).to_string()), + actor_type: sea_orm::ActiveValue::Set(object.actor_type().ok_or(super::FieldError("type"))?), + name: sea_orm::ActiveValue::Set(object.name().map(|x| x.to_string())), + summary: sea_orm::ActiveValue::Set(object.summary().map(|x| x.to_string())), + icon: sea_orm::ActiveValue::Set(object.icon().get().and_then(|x| x.url().id())), + image: sea_orm::ActiveValue::Set(object.image().get().and_then(|x| x.url().id())), + inbox: sea_orm::ActiveValue::Set(object.inbox().id()), + outbox: sea_orm::ActiveValue::Set(object.outbox().id()), + shared_inbox: sea_orm::ActiveValue::Set(object.endpoints().get().and_then(|x| Some(x.shared_inbox()?.to_string()))), + followers: sea_orm::ActiveValue::Set(object.followers().id()), + following: sea_orm::ActiveValue::Set(object.following().id()), + published: sea_orm::ActiveValue::Set(object.published().unwrap_or(chrono::Utc::now())), + updated: sea_orm::ActiveValue::Set(chrono::Utc::now()), + following_count: sea_orm::ActiveValue::Set(object.following_count().unwrap_or(0) as i32), + followers_count: sea_orm::ActiveValue::Set(object.followers_count().unwrap_or(0) as i32), + statuses_count: sea_orm::ActiveValue::Set(object.statuses_count().unwrap_or(0) as i32), + public_key: sea_orm::ActiveValue::Set(object.public_key().get().ok_or(super::FieldError("publicKey"))?.public_key_pem().to_string()), + private_key: sea_orm::ActiveValue::Set(None), // there's no way to transport privkey over AP json, must come from DB + }) + } +} + +impl Model { + pub fn ap(self) -> serde_json::Value { + serde_json::Value::new_object() + .set_id(Some(&self.id)) + .set_actor_type(Some(self.actor_type)) + .set_name(self.name.as_deref()) + .set_summary(self.summary.as_deref()) + .set_icon(apb::Node::maybe_object(self.icon.map(|i| + serde_json::Value::new_object() + .set_document_type(Some(apb::DocumentType::Image)) + .set_url(apb::Node::link(i.clone())) + ))) + .set_image(apb::Node::maybe_object(self.image.map(|i| + serde_json::Value::new_object() + .set_document_type(Some(apb::DocumentType::Image)) + .set_url(apb::Node::link(i.clone())) + ))) + .set_published(Some(self.published)) + .set_preferred_username(Some(&self.preferred_username)) + .set_statuses_count(Some(self.statuses_count as u64)) + .set_followers_count(Some(self.followers_count as u64)) + .set_following_count(Some(self.following_count as u64)) + .set_inbox(apb::Node::maybe_link(self.inbox)) + .set_outbox(apb::Node::maybe_link(self.outbox)) + .set_following(apb::Node::maybe_link(self.following)) + .set_followers(apb::Node::maybe_link(self.followers)) + .set_public_key(apb::Node::object( + serde_json::Value::new_object() + .set_id(Some(&format!("{}#main-key", self.id))) + .set_owner(Some(&self.id)) + .set_public_key_pem(&self.public_key) + )) + .set_endpoints(apb::Node::object( + serde_json::Value::new_object() + .set_shared_inbox(self.shared_inbox.as_deref()) + )) + .set_discoverable(Some(true)) + } +} + +fn split_user_id(id: &str) -> (String, String) { + let clean = id + .replace("http://", "") + .replace("https://", ""); + let mut splits = clean.split('/'); + let first = splits.next().unwrap_or(""); + let last = splits.last().unwrap_or(first); + (first.to_string(), last.to_string()) +} diff --git a/src/model/addressing.rs b/src/model/addressing.rs index fe85556..8bbd139 100644 --- a/src/model/addressing.rs +++ b/src/model/addressing.rs @@ -7,53 +7,71 @@ use crate::routes::activitypub::jsonld::LD; #[sea_orm(table_name = "addressing")] pub struct Model { #[sea_orm(primary_key)] - pub id: i64, - pub actor: String, - pub server: String, - pub activity: Option, - pub object: Option, + pub internal: i64, + pub actor: Option, + pub instance: Option, + pub activity: Option, + pub object: Option, pub published: ChronoDateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::Actor", - to = "super::user::Column::Id" - )] - User, - #[sea_orm( belongs_to = "super::activity::Entity", from = "Column::Activity", - to = "super::activity::Column::Id" + to = "super::activity::Column::Internal", + on_update = "Cascade", + on_delete = "NoAction" )] - Activity, - + Activities, + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Internal", + on_update = "Cascade", + on_delete = "NoAction" + )] + Actors, + #[sea_orm( + belongs_to = "super::instance::Entity", + from = "Column::Instance", + to = "super::instance::Column::Internal", + on_update = "Cascade", + on_delete = "NoAction" + )] + Instances, #[sea_orm( belongs_to = "super::object::Entity", from = "Column::Object", - to = "super::object::Column::Id" + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "NoAction" )] - Object, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } + Objects, } impl Related for Entity { fn to() -> RelationDef { - Relation::Activity.def() + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Instances.def() } } impl Related for Entity { fn to() -> RelationDef { - Relation::Object.def() + Relation::Objects.def() } } @@ -79,12 +97,12 @@ pub enum Event { impl Event { - pub fn id(&self) -> &str { + pub fn internal(&self) -> i64 { match self { - Event::Tombstone => "", - Event::Activity(x) => x.id.as_str(), - Event::StrayObject { object, liked: _ } => object.id.as_str(), - Event::DeepActivity { activity: _, liked: _, object } => object.id.as_str(), + Event::Tombstone => 0, + Event::Activity(x) => x.internal, + Event::StrayObject { object, liked: _ } => object.internal, + Event::DeepActivity { activity: _, liked: _, object } => object.internal, } } @@ -136,12 +154,12 @@ impl FromQueryResult for Event { impl Entity { - pub fn find_addressed(uid: Option<&str>) -> Select { + pub fn find_addressed(uid: Option) -> Select { let mut select = Entity::find() .distinct() .select_only() - .join(sea_orm::JoinType::LeftJoin, Relation::Object.def()) - .join(sea_orm::JoinType::LeftJoin, Relation::Activity.def()) + .join(sea_orm::JoinType::LeftJoin, Relation::Objects.def()) + .join(sea_orm::JoinType::LeftJoin, Relation::Activities.def()) .filter( // TODO ghetto double inner join because i want to filter out tombstones Condition::any() @@ -151,12 +169,11 @@ impl Entity { .order_by(Column::Published, Order::Desc); if let Some(uid) = uid { - let uid = uid.to_string(); select = select .join( sea_orm::JoinType::LeftJoin, - crate::model::object::Relation::Like.def() - .on_condition(move |_l, _r| crate::model::like::Column::Actor.eq(uid.clone()).into_condition()), + crate::model::object::Relation::Likes.def() + .on_condition(move |_l, _r| crate::model::like::Column::Actor.eq(uid).into_condition()), ) .select_column_as(crate::model::like::Column::Actor, format!("{}{}", crate::model::like::Entity.table_name(), crate::model::like::Column::Actor.to_string())); } diff --git a/src/model/announce.rs b/src/model/announce.rs new file mode 100644 index 0000000..5a800b4 --- /dev/null +++ b/src/model/announce.rs @@ -0,0 +1,45 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "announces")] +pub struct Model { + #[sea_orm(primary_key)] + pub internal: i64, + pub actor: i64, + pub object: i64, + pub published: ChronoDateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Actors, + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/application.rs b/src/model/application.rs deleted file mode 100644 index cde98aa..0000000 --- a/src/model/application.rs +++ /dev/null @@ -1,18 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "application")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - - pub private_key: String, - pub public_key: String, - - pub created: ChronoDateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/attachment.rs b/src/model/attachment.rs index 1a11796..3956819 100644 --- a/src/model/attachment.rs +++ b/src/model/attachment.rs @@ -1,4 +1,4 @@ -use apb::{DocumentMut, ObjectMut}; +use apb::{DocumentMut, DocumentType, ObjectMut}; use sea_orm::entity::prelude::*; use crate::routes::activitypub::jsonld::LD; @@ -9,16 +9,36 @@ use super::addressing::Event; #[sea_orm(table_name = "attachments")] pub struct Model { #[sea_orm(primary_key)] - pub id: i64, - + pub internal: i64, + #[sea_orm(unique)] pub url: String, - pub object: String, - pub document_type: apb::DocumentType, + pub object: i64, + pub document_type: DocumentType, pub name: Option, pub media_type: String, - pub created: ChronoDateTimeUtc, + pub published: ChronoDateTimeUtc, } +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + impl Model { pub fn ap(self) -> serde_json::Value { serde_json::Value::new_object() @@ -26,37 +46,18 @@ impl Model { .set_document_type(Some(self.document_type)) .set_media_type(Some(&self.media_type)) .set_name(self.name.as_deref()) - .set_published(Some(self.created)) + .set_published(Some(self.published)) } } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::object::Entity", - from = "Column::Object", - to = "super::object::Column::Id" - )] - Object, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Object.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - - #[axum::async_trait] pub trait BatchFillable { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr>; + async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr>; } #[axum::async_trait] impl BatchFillable for &[Event] { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { + async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { let objects : Vec = self .iter() .filter_map(|x| match x { @@ -69,12 +70,11 @@ impl BatchFillable for &[Event] { let attachments = objects.load_many(Entity, db).await?; - let mut out : std::collections::BTreeMap> = std::collections::BTreeMap::new(); + let mut out : std::collections::BTreeMap> = std::collections::BTreeMap::new(); for attach in attachments.into_iter().flatten() { - if out.contains_key(&attach.object) { - out.get_mut(&attach.object).expect("contains but get failed?").push(attach); - } else { - out.insert(attach.object.clone(), vec![attach]); + match out.entry(attach.object) { + std::collections::btree_map::Entry::Vacant(a) => { a.insert(vec![attach]); }, + std::collections::btree_map::Entry::Occupied(mut e) => { e.get_mut().push(attach); }, } } @@ -84,14 +84,14 @@ impl BatchFillable for &[Event] { #[axum::async_trait] impl BatchFillable for Vec { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { + async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { self.as_slice().load_attachments_batch(db).await } } #[axum::async_trait] impl BatchFillable for Event { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { + async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { let x = vec![self.clone()]; // TODO wasteful clone and vec![] but ehhh convenient x.load_attachments_batch(db).await } diff --git a/src/model/config.rs b/src/model/config.rs index b34df1d..5335dec 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -4,7 +4,9 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "configs")] pub struct Model { #[sea_orm(primary_key)] - pub id: String, + pub internal: i64, + #[sea_orm(unique)] + pub actor: String, pub accept_follow_requests: bool, pub show_followers_count: bool, pub show_following_count: bool, @@ -15,7 +17,7 @@ pub struct Model { impl Default for Model { fn default() -> Self { Model { - id: "".to_string(), + internal: 0, actor: "".into(), accept_follow_requests: true, show_following_count: true, show_following: true, @@ -28,16 +30,18 @@ impl Default for Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::Id", - to = "super::user::Column::Id" + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" )] - User, + Actors, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::User.def() + Relation::Actors.def() } } diff --git a/src/model/credential.rs b/src/model/credential.rs index f3cdc5f..be2d2c6 100644 --- a/src/model/credential.rs +++ b/src/model/credential.rs @@ -4,24 +4,28 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "credentials")] pub struct Model { #[sea_orm(primary_key)] - pub id: String, - pub email: String, + pub internal: i64, + #[sea_orm(unique)] + pub actor: String, + pub login: String, pub password: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::Id", - to = "super::user::Column::Id" + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" )] - User, + Actors, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::User.def() + Relation::Actors.def() } } diff --git a/src/model/delivery.rs b/src/model/delivery.rs index ef71261..b767f67 100644 --- a/src/model/delivery.rs +++ b/src/model/delivery.rs @@ -4,11 +4,11 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "deliveries")] pub struct Model { #[sea_orm(primary_key)] - pub id: i64, + pub internal: i64, pub actor: String, pub target: String, pub activity: String, - pub created: ChronoDateTimeUtc, + pub published: ChronoDateTimeUtc, pub not_before: ChronoDateTimeUtc, pub attempt: i32, } @@ -18,14 +18,30 @@ pub enum Relation { #[sea_orm( belongs_to = "super::activity::Entity", from = "Column::Activity", - to = "super::activity::Column::Id" + to = "super::activity::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" )] - Activity, + Activities, + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Actors, } impl Related for Entity { fn to() -> RelationDef { - Relation::Activity.def() + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() } } @@ -45,6 +61,6 @@ impl Model { } pub fn expired(&self) -> bool { - chrono::Utc::now() - self.created > chrono::Duration::days(7) + chrono::Utc::now() - self.published > chrono::Duration::days(7) } } diff --git a/src/model/share.rs b/src/model/hashtag.rs similarity index 58% rename from src/model/share.rs rename to src/model/hashtag.rs index e677cf9..bcd603b 100644 --- a/src/model/share.rs +++ b/src/model/hashtag.rs @@ -1,28 +1,30 @@ use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "shares")] +#[sea_orm(table_name = "hashtags")] pub struct Model { #[sea_orm(primary_key)] - pub id: i64, - pub actor: String, - pub shares: String, - pub date: ChronoDateTimeUtc, + pub internal: i64, + pub object: i64, + pub name: String, + pub published: ChronoDateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( belongs_to = "super::object::Entity", - from = "Column::Shares", - to = "super::object::Column::Id", + from = "Column::Object", + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" )] - Object + Objects, } impl Related for Entity { fn to() -> RelationDef { - Relation::Object.def() + Relation::Objects.def() } } diff --git a/src/model/instance.rs b/src/model/instance.rs new file mode 100644 index 0000000..58039e5 --- /dev/null +++ b/src/model/instance.rs @@ -0,0 +1,70 @@ +use nodeinfo::NodeInfoOwned; +use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; + +use crate::errors::UpubError; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "instances")] +pub struct Model { + #[sea_orm(primary_key)] + pub internal: i64, + #[sea_orm(unique)] + pub domain: String, + pub name: Option, + pub software: Option, + pub version: Option, + pub icon: Option, + pub down_since: Option, + pub users: Option, + pub posts: Option, + pub published: ChronoDateTimeUtc, + pub updated: ChronoDateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::actor::Entity")] + Actors, + #[sea_orm(has_many = "super::addressing::Entity")] + Addressing, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Addressing.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_domain(domain: &str) -> Select { + Entity::find().filter(Column::Domain.eq(domain)) + } + + pub async fn domain_to_internal(domain: &str, db: &DatabaseConnection) -> crate::Result { + Entity::find() + .filter(Column::Domain.eq(domain)) + .select_only() + .select_column(Column::Internal) + .into_tuple::() + .one(db) + .await? + .ok_or_else(UpubError::not_found) + } + + pub async fn nodeinfo(domain: &str) -> crate::Result { + Ok( + reqwest::get(format!("https://{domain}/nodeinfo/2.0.json")) + .await? + .json() + .await? + ) + } +} diff --git a/src/model/like.rs b/src/model/like.rs index 2e072f6..c92f2b8 100644 --- a/src/model/like.rs +++ b/src/model/like.rs @@ -4,26 +4,63 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "likes")] pub struct Model { #[sea_orm(primary_key)] - pub id: i64, - pub actor: String, - pub likes: String, - pub date: ChronoDateTimeUtc, + pub internal: i64, + pub actor: i64, + pub object: i64, + pub activity: i64, + pub published: ChronoDateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::object::Entity", - from = "Column::Likes", - to = "super::object::Column::Id", + belongs_to = "super::activity::Entity", + from = "Column::Activity", + to = "super::activity::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" )] - Object + Activities, + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Actors, + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } } impl Related for Entity { fn to() -> RelationDef { - Relation::Object.def() + Relation::Objects.def() } } impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_uid_oid(uid: i64, oid: i64) -> Select { + Entity::find().filter(Column::Actor.eq(uid)).filter(Column::Object.eq(oid)) + } +} diff --git a/src/model/mention.rs b/src/model/mention.rs new file mode 100644 index 0000000..f08cb04 --- /dev/null +++ b/src/model/mention.rs @@ -0,0 +1,45 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "mentions")] +pub struct Model { + #[sea_orm(primary_key)] + pub internal: i64, + pub object: i64, + pub actor: String, + pub published: ChronoDateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Actors, + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/mod.rs b/src/model/mod.rs index 04bdb6d..dc5fcf8 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,18 +1,23 @@ +pub mod actor; pub mod object; pub mod activity; -pub mod user; -pub mod config; -pub mod relay; -pub mod relation; -pub mod addressing; -pub mod share; -pub mod like; +pub mod config; pub mod credential; pub mod session; + +pub mod instance; pub mod delivery; + +pub mod relation; +pub mod announce; +pub mod like; + +pub mod hashtag; +pub mod mention; pub mod attachment; -pub mod application; + +pub mod addressing; #[derive(Debug, Clone, thiserror::Error)] #[error("missing required field: '{0}'")] diff --git a/src/model/object.rs b/src/model/object.rs index 82387a8..4f29637 100644 --- a/src/model/object.rs +++ b/src/model/object.rs @@ -1,7 +1,7 @@ -use apb::{BaseMut, Collection, CollectionMut, ObjectMut}; -use sea_orm::entity::prelude::*; +use apb::{BaseMut, Collection, CollectionMut, ObjectMut, ObjectType}; +use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; -use crate::routes::activitypub::jsonld::LD; +use crate::{errors::UpubError, routes::activitypub::jsonld::LD}; use super::Audience; @@ -9,57 +9,181 @@ use super::Audience; #[sea_orm(table_name = "objects")] pub struct Model { #[sea_orm(primary_key)] + pub internal: i64, + #[sea_orm(unique)] pub id: String, - pub object_type: apb::ObjectType, + pub object_type: ObjectType, pub attributed_to: Option, pub name: Option, pub summary: Option, pub content: Option, - pub likes: i64, - pub shares: i64, - pub comments: i64, - pub context: Option, + pub sensitive: bool, pub in_reply_to: Option, - pub cc: Audience, - pub bcc: Audience, + pub url: Option, + pub likes: i32, + pub announces: i32, + pub replies: i32, + pub context: Option, pub to: Audience, pub bto: Audience, - pub url: Option, + pub cc: Audience, + pub bcc: Audience, pub published: ChronoDateTimeUtc, - pub updated: Option, + pub updated: ChronoDateTimeUtc, +} - pub sensitive: bool, +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::activity::Entity")] + Activities, + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::AttributedTo", + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "NoAction" + )] + Actors, + #[sea_orm(has_many = "super::addressing::Entity")] + Addressing, + #[sea_orm(has_many = "super::announce::Entity")] + Announces, + #[sea_orm(has_many = "super::attachment::Entity")] + Attachments, + #[sea_orm(has_many = "super::hashtag::Entity")] + Hashtags, + #[sea_orm(has_many = "super::like::Entity")] + Likes, + #[sea_orm(has_many = "super::mention::Entity")] + Mentions, + #[sea_orm( + belongs_to = "Entity", + from = "Column::InReplyTo", + to = "Column::Id", + on_update = "Cascade", + on_delete = "NoAction" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Addressing.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Announces.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Attachments.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Hashtags.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Likes.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Mentions.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_ap_id(id: &str) -> Select { + Entity::find().filter(Column::Id.eq(id)) + } + + pub fn delete_by_ap_id(id: &str) -> sea_orm::DeleteMany { + Entity::delete_many().filter(Column::Id.eq(id)) + } + + pub async fn ap_to_internal(id: &str, db: &DatabaseConnection) -> crate::Result { + Entity::find() + .filter(Column::Id.eq(id)) + .select_only() + .select_column(Column::Internal) + .into_tuple::() + .one(db) + .await? + .ok_or_else(UpubError::not_found) + } +} + +impl ActiveModel { + pub fn new(object: &impl apb::Object) -> Result { + let t = object.object_type().ok_or(super::FieldError("type"))?; + if matches!(t, + apb::ObjectType::Activity(_) + | apb::ObjectType::Actor(_) + | apb::ObjectType::Collection(_) + | apb::ObjectType::Document(_) + ) { + return Err(super::FieldError("type")); + } + Ok(ActiveModel { + internal: sea_orm::ActiveValue::NotSet, + id: sea_orm::ActiveValue::Set(object.id().ok_or(super::FieldError("id"))?.to_string()), + object_type: sea_orm::ActiveValue::Set(t), + attributed_to: sea_orm::ActiveValue::Set(object.attributed_to().id()), + name: sea_orm::ActiveValue::Set(object.name().map(|x| x.to_string())), + summary: sea_orm::ActiveValue::Set(object.summary().map(|x| x.to_string())), + content: sea_orm::ActiveValue::Set(object.content().map(|x| x.to_string())), + context: sea_orm::ActiveValue::Set(object.context().id()), + in_reply_to: sea_orm::ActiveValue::Set(object.in_reply_to().id()), + published: sea_orm::ActiveValue::Set(object.published().unwrap_or_else(chrono::Utc::now)), + updated: sea_orm::ActiveValue::Set(object.updated().unwrap_or_else(chrono::Utc::now)), + url: sea_orm::ActiveValue::Set(object.url().id()), + replies: sea_orm::ActiveValue::Set(object.replies().get() + .map_or(0, |x| x.total_items().unwrap_or(0)) as i32), + likes: sea_orm::ActiveValue::Set(object.likes().get() + .map_or(0, |x| x.total_items().unwrap_or(0)) as i32), + announces: sea_orm::ActiveValue::Set(object.shares().get() + .map_or(0, |x| x.total_items().unwrap_or(0)) as i32), + to: sea_orm::ActiveValue::Set(object.to().into()), + bto: sea_orm::ActiveValue::Set(object.bto().into()), + cc: sea_orm::ActiveValue::Set(object.cc().into()), + bcc: sea_orm::ActiveValue::Set(object.bcc().into()), + + sensitive: sea_orm::ActiveValue::Set(object.sensitive().unwrap_or(false)), + }) + } } impl Model { - pub fn new(object: &impl apb::Object) -> Result { - Ok(Model { - id: object.id().ok_or(super::FieldError("id"))?.to_string(), - object_type: object.object_type().ok_or(super::FieldError("type"))?, - attributed_to: object.attributed_to().id(), - name: object.name().map(|x| x.to_string()), - summary: object.summary().map(|x| x.to_string()), - content: object.content().map(|x| x.to_string()), - context: object.context().id(), - in_reply_to: object.in_reply_to().id(), - published: object.published().ok_or(super::FieldError("published"))?, - updated: object.updated(), - url: object.url().id(), - comments: object.replies().get() - .map_or(0, |x| x.total_items().unwrap_or(0)) as i64, - likes: object.likes().get() - .map_or(0, |x| x.total_items().unwrap_or(0)) as i64, - shares: object.shares().get() - .map_or(0, |x| x.total_items().unwrap_or(0)) as i64, - to: object.to().into(), - bto: object.bto().into(), - cc: object.cc().into(), - bcc: object.bcc().into(), - - sensitive: object.sensitive().unwrap_or(false), - }) - } - pub fn ap(self) -> serde_json::Value { serde_json::Value::new_object() .set_id(Some(&self.id)) @@ -72,7 +196,7 @@ impl Model { .set_conversation(apb::Node::maybe_link(self.context.clone())) // duplicate context for mastodon .set_in_reply_to(apb::Node::maybe_link(self.in_reply_to.clone())) .set_published(Some(self.published)) - .set_updated(self.updated) + .set_updated(Some(self.updated)) .set_to(apb::Node::links(self.to.0.clone())) .set_bto(apb::Node::Empty) .set_cc(apb::Node::links(self.cc.0.clone())) @@ -82,7 +206,7 @@ impl Model { .set_shares(apb::Node::object( serde_json::Value::new_object() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(self.shares as u64)) + .set_total_items(Some(self.announces as u64)) )) .set_likes(apb::Node::object( serde_json::Value::new_object() @@ -92,74 +216,11 @@ impl Model { .set_replies(apb::Node::object( serde_json::Value::new_object() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(self.comments as u64)) + .set_total_items(Some(self.replies as u64)) )) } } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::activity::Entity")] - Activity, - - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::AttributedTo", - to = "super::user::Column::Id", - )] - User, - - #[sea_orm(has_many = "super::addressing::Entity")] - Addressing, - - #[sea_orm(has_many = "super::attachment::Entity")] - Attachment, - - #[sea_orm(has_many = "super::like::Entity")] - Like, - - #[sea_orm(has_many = "super::share::Entity")] - Share, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Activity.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Addressing.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Attachment.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Like.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Share.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - impl apb::target::Addressed for Model { fn addressed(&self) -> Vec { let mut to : Vec = self.to.0.clone(); diff --git a/src/model/relation.rs b/src/model/relation.rs index 41bcc27..724e398 100644 --- a/src/model/relation.rs +++ b/src/model/relation.rs @@ -1,16 +1,119 @@ -use sea_orm::entity::prelude::*; +use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "relations")] pub struct Model { #[sea_orm(primary_key)] - pub id: i64, - pub follower: String, - pub following: String, + pub internal: i64, + pub follower: i64, + pub following: i64, + pub accept: Option, + pub activity: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} -// TODO how to represent this User-to-User relation in sea orm?? +pub enum Relation { + #[sea_orm( + belongs_to = "super::activity::Entity", + from = "Column::Accept", + to = "super::activity::Column::Internal", + on_update = "Cascade", + on_delete = "NoAction" + )] + ActivitiesAccept, + #[sea_orm( + belongs_to = "super::activity::Entity", + from = "Column::Activity", + to = "super::activity::Column::Internal", + on_update = "Cascade", + on_delete = "NoAction" + )] + ActivitiesFollow, + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Follower", + to = "super::actor::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + ActorsFollower, + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Following", + to = "super::actor::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + ActorsFollowing, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActorsFollowing.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActivitiesFollow.def() + } +} impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + // TODO this is 2 queries!!! can it be optimized down to 1? + pub async fn followers(uid: &str, db: &DatabaseConnection) -> crate::Result> { + let internal_id = super::actor::Entity::ap_to_internal(uid, db).await?; + let out = Entity::find() + .join( + sea_orm::JoinType::InnerJoin, + Entity::belongs_to(super::actor::Entity) + .from(Column::Follower) + .to(super::actor::Column::Internal) + .into() + ) + .filter(Column::Accept.is_not_null()) + .filter(Column::Following.eq(internal_id)) + .select_only() + .select_column(super::actor::Column::Id) + .into_tuple::() + .all(db) + .await?; + + Ok(out) + } + + // TODO this is 2 queries!!! can it be optimized down to 1? + pub async fn following(uid: &str, db: &DatabaseConnection) -> crate::Result> { + let internal_id = super::actor::Entity::ap_to_internal(uid, db).await?; + let out = Entity::find() + .join( + sea_orm::JoinType::InnerJoin, + Entity::belongs_to(super::actor::Entity) + .from(Column::Following) + .to(super::actor::Column::Internal) + .into() + ) + .filter(Column::Accept.is_not_null()) + .filter(Column::Follower.eq(internal_id)) + .select_only() + .select_column(super::actor::Column::Id) + .into_tuple::() + .all(db) + .await?; + + Ok(out) + } + + // TODO this is 3 queries!!! can it be optimized down to 1? + pub fn is_following(follower: i64, following: i64) -> sea_orm::Selector> { + Entity::find() + .filter(Column::Accept.is_not_null()) + .filter(Column::Follower.eq(follower)) + .filter(Column::Following.eq(following)) + .select_only() + .select_column(Column::Internal) + .into_tuple::() + } +} diff --git a/src/model/relay.rs b/src/model/relay.rs deleted file mode 100644 index 0ea3b59..0000000 --- a/src/model/relay.rs +++ /dev/null @@ -1,16 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "relays")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: String, - pub accepted: bool, - pub forwarding: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} -// TODO how to represent this User-to-User relation in sea orm?? - -impl ActiveModelBehavior for ActiveModel {} diff --git a/src/model/session.rs b/src/model/session.rs index a2216e2..f8ba460 100644 --- a/src/model/session.rs +++ b/src/model/session.rs @@ -4,24 +4,27 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "sessions")] pub struct Model { #[sea_orm(primary_key)] - pub id: String, + pub internal: i64, pub actor: String, + pub secret: String, pub expires: ChronoDateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::user::Entity", + belongs_to = "super::actor::Entity", from = "Column::Actor", - to = "super::user::Column::Id" + to = "super::actor::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" )] - User, + Actors, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::User.def() + Relation::Actors.def() } } diff --git a/src/model/user.rs b/src/model/user.rs deleted file mode 100644 index 9aaf341..0000000 --- a/src/model/user.rs +++ /dev/null @@ -1,176 +0,0 @@ -use sea_orm::entity::prelude::*; - -use apb::{Actor, ActorMut, ActorType, BaseMut, DocumentMut, Endpoints, EndpointsMut, Object, ObjectMut, PublicKey, PublicKeyMut}; - -use crate::routes::activitypub::jsonld::LD; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "users")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: String, - pub domain: String, - pub actor_type: ActorType, - pub preferred_username: String, - - pub name: Option, - pub summary: Option, - pub image: Option, - pub icon: Option, - - pub inbox: Option, - pub shared_inbox: Option, - pub outbox: Option, - pub following: Option, - pub followers: Option, - - pub following_count: i64, - pub followers_count: i64, - pub statuses_count: i64, - - pub public_key: String, - pub private_key: Option, - - pub created: ChronoDateTimeUtc, - pub updated: ChronoDateTimeUtc, - - // TODO these are also suggested - // pub liked: Option, - // pub streams: Option, -} - -impl Model { - pub fn new(object: &impl Actor) -> Result { - let ap_id = object.id().ok_or(super::FieldError("id"))?.to_string(); - let (domain, fallback_preferred_username) = split_user_id(&ap_id); - Ok(Model { - id: ap_id, - domain, - preferred_username: object.preferred_username().unwrap_or(&fallback_preferred_username).to_string(), - actor_type: object.actor_type().ok_or(super::FieldError("type"))?, - name: object.name().map(|x| x.to_string()), - summary: object.summary().map(|x| x.to_string()), - icon: object.icon().get().and_then(|x| x.url().id()), - image: object.image().get().and_then(|x| x.url().id()), - inbox: object.inbox().id(), - outbox: object.outbox().id(), - shared_inbox: object.endpoints().get().and_then(|x| Some(x.shared_inbox()?.to_string())), - followers: object.followers().id(), - following: object.following().id(), - created: object.published().unwrap_or(chrono::Utc::now()), - updated: chrono::Utc::now(), - following_count: object.following_count().unwrap_or(0) as i64, - followers_count: object.followers_count().unwrap_or(0) as i64, - statuses_count: object.statuses_count().unwrap_or(0) as i64, - public_key: object.public_key().get().ok_or(super::FieldError("publicKey"))?.public_key_pem().to_string(), - private_key: None, // there's no way to transport privkey over AP json, must come from DB - }) - } - - pub fn ap(self) -> serde_json::Value { - serde_json::Value::new_object() - .set_id(Some(&self.id)) - .set_actor_type(Some(self.actor_type)) - .set_name(self.name.as_deref()) - .set_summary(self.summary.as_deref()) - .set_icon(apb::Node::maybe_object(self.icon.map(|i| - serde_json::Value::new_object() - .set_document_type(Some(apb::DocumentType::Image)) - .set_url(apb::Node::link(i.clone())) - ))) - .set_image(apb::Node::maybe_object(self.image.map(|i| - serde_json::Value::new_object() - .set_document_type(Some(apb::DocumentType::Image)) - .set_url(apb::Node::link(i.clone())) - ))) - .set_published(Some(self.created)) - .set_preferred_username(Some(&self.preferred_username)) - .set_statuses_count(Some(self.statuses_count as u64)) - .set_followers_count(Some(self.followers_count as u64)) - .set_following_count(Some(self.following_count as u64)) - .set_inbox(apb::Node::maybe_link(self.inbox)) - .set_outbox(apb::Node::maybe_link(self.outbox)) - .set_following(apb::Node::maybe_link(self.following)) - .set_followers(apb::Node::maybe_link(self.followers)) - .set_public_key(apb::Node::object( - serde_json::Value::new_object() - .set_id(Some(&format!("{}#main-key", self.id))) - .set_owner(Some(&self.id)) - .set_public_key_pem(&self.public_key) - )) - .set_endpoints(apb::Node::object( - serde_json::Value::new_object() - .set_shared_inbox(self.shared_inbox.as_deref()) - )) - .set_discoverable(Some(true)) - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::activity::Entity")] - Activity, - - #[sea_orm(has_many = "super::object::Entity")] - Object, - - #[sea_orm(has_one = "super::config::Entity")] - Config, - - #[sea_orm(has_one = "super::credential::Entity")] - Credential, - - #[sea_orm(has_many = "super::session::Entity")] - Session, - - #[sea_orm(has_many = "super::addressing::Entity")] - Addressing, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Activity.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Object.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Config.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Credential.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Session.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Addressing.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -fn split_user_id(id: &str) -> (String, String) { - let clean = id - .replace("http://", "") - .replace("https://", ""); - let mut splits = clean.split('/'); - let first = splits.next().unwrap_or(""); - let last = splits.last().unwrap_or(first); - (first.to_string(), last.to_string()) -} diff --git a/src/routes/activitypub/activity.rs b/src/routes/activitypub/activity.rs index bc15a35..a7880bc 100644 --- a/src/routes/activitypub/activity.rs +++ b/src/routes/activitypub/activity.rs @@ -27,7 +27,7 @@ pub async fn view( .ok_or_else(UpubError::not_found)?; let mut attachments = row.load_attachments_batch(ctx.db()).await?; - let attach = attachments.remove(row.id()); + let attach = attachments.remove(&row.internal()); Ok(JsonLD(row.ap(attach).ld_context())) } diff --git a/src/routes/activitypub/application.rs b/src/routes/activitypub/application.rs index 7d9aa4e..fdadcd2 100644 --- a/src/routes/activitypub/application.rs +++ b/src/routes/activitypub/application.rs @@ -2,7 +2,7 @@ use apb::{ActorMut, BaseMut, ObjectMut, PublicKeyMut}; use axum::{extract::{Query, State}, http::HeaderMap, response::{IntoResponse, Redirect, Response}, Form, Json}; use reqwest::Method; -use crate::{errors::UpubError, server::{auth::{AuthIdentity, Identity}, fetcher::Fetcher, Context}, url}; +use crate::{errors::UpubError, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url}; use super::{jsonld::LD, JsonLD}; @@ -26,14 +26,14 @@ pub async fn view( .set_summary(Some(&ctx.cfg().instance.description)) .set_inbox(apb::Node::link(url!(ctx, "/inbox"))) .set_outbox(apb::Node::link(url!(ctx, "/outbox"))) - .set_published(Some(ctx.app().created)) + .set_published(Some(ctx.actor().published)) .set_endpoints(apb::Node::Empty) .set_preferred_username(Some(ctx.domain())) .set_public_key(apb::Node::object( serde_json::Value::new_object() .set_id(Some(&url!(ctx, "#main-key"))) .set_owner(Some(&url!(ctx, ""))) - .set_public_key_pem(&ctx.app().public_key) + .set_public_key_pem(&ctx.actor().public_key) )) .ld_context() ).into_response()) @@ -50,7 +50,7 @@ pub async fn proxy_get( AuthIdentity(auth): AuthIdentity, ) -> crate::Result> { // only local users can request fetches - if !ctx.cfg().security.allow_public_debugger && !matches!(auth, Identity::Local(_)) { + if !ctx.cfg().security.allow_public_debugger && !auth.is_local() { return Err(UpubError::unauthorized()); } Ok(Json( @@ -59,7 +59,7 @@ pub async fn proxy_get( &query.id, None, ctx.base(), - &ctx.app().private_key, + ctx.pkey(), &format!("{}+proxy", ctx.domain()), ) .await? @@ -74,7 +74,7 @@ pub async fn proxy_form( Form(query): Form, ) -> crate::Result> { // only local users can request fetches - if !ctx.cfg().security.allow_public_debugger && !matches!(auth, Identity::Local(_)) { + if !ctx.cfg().security.allow_public_debugger && auth.is_local() { return Err(UpubError::unauthorized()); } Ok(Json( @@ -83,7 +83,7 @@ pub async fn proxy_form( &query.id, None, ctx.base(), - &ctx.app().private_key, + ctx.pkey(), &format!("{}+proxy", ctx.domain()), ) .await? diff --git a/src/routes/activitypub/auth.rs b/src/routes/activitypub/auth.rs index acb01b5..76dd5a8 100644 --- a/src/routes/activitypub/auth.rs +++ b/src/routes/activitypub/auth.rs @@ -1,6 +1,6 @@ use axum::{http::StatusCode, extract::State, Json}; use rand::Rng; -use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; +use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, Condition, EntityTrait, QueryFilter}; use crate::{errors::UpubError, model, server::{admin::Administrable, Context}}; @@ -18,6 +18,15 @@ pub struct AuthSuccess { expires: chrono::DateTime, } +fn token() -> String { + // TODO should probably use crypto-safe rng + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(128) + .map(char::from) + .collect() +} + pub async fn login( State(ctx): State, Json(login): Json @@ -25,24 +34,20 @@ pub async fn login( // TODO salt the pwd match model::credential::Entity::find() .filter(Condition::all() - .add(model::credential::Column::Email.eq(login.email)) + .add(model::credential::Column::Login.eq(login.email)) .add(model::credential::Column::Password.eq(sha256::digest(login.password))) ) .one(ctx.db()) .await? { Some(x) => { - // TODO should probably use crypto-safe rng - let token : String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(128) - .map(char::from) - .collect(); + let token = token(); let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6); model::session::Entity::insert( model::session::ActiveModel { - id: sea_orm::ActiveValue::Set(token.clone()), - actor: sea_orm::ActiveValue::Set(x.id.clone()), + internal: sea_orm::ActiveValue::NotSet, + secret: sea_orm::ActiveValue::Set(token.clone()), + actor: sea_orm::ActiveValue::Set(x.actor.clone()), expires: sea_orm::ActiveValue::Set(expires), } ) @@ -50,13 +55,52 @@ pub async fn login( .await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(AuthSuccess { token, expires, - user: x.id + user: x.actor })) }, None => Err(UpubError::unauthorized()), } } +#[derive(Debug, Clone, serde::Deserialize)] +pub struct RefreshForm { + token: String, +} + +pub async fn refresh( + State(ctx): State, + Json(login): Json +) -> crate::Result> { + if !ctx.cfg().security.allow_login_refresh { + return Err(UpubError::forbidden()); + } + + let prev = model::session::Entity::find() + .filter(model::session::Column::Secret.eq(login.token)) + .one(ctx.db()) + .await? + .ok_or_else(UpubError::unauthorized)?; + + if prev.expires > chrono::Utc::now() { + return Ok(Json(AuthSuccess { token: prev.secret, user: prev.actor, expires: prev.expires })); + } + + let token = token(); + let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6); + let user = prev.actor; + let new_session = model::session::ActiveModel { + internal: NotSet, + actor: Set(user.clone()), + secret: Set(token.clone()), + expires: Set(expires), + }; + model::session::Entity::insert(new_session) + .exec(ctx.db()) + .await?; + + Ok(Json(AuthSuccess { token, expires, user })) +} + #[derive(Debug, Clone, serde::Deserialize)] pub struct RegisterForm { username: String, diff --git a/src/routes/activitypub/context.rs b/src/routes/activitypub/context.rs index 42422ea..360d21f 100644 --- a/src/routes/activitypub/context.rs +++ b/src/routes/activitypub/context.rs @@ -9,7 +9,7 @@ pub async fn get( AuthIdentity(auth): AuthIdentity, ) -> crate::Result> { let local_context_id = url!(ctx, "/context/{id}"); - let context = ctx.context_id(&id); + let context = ctx.oid(&id); let count = model::addressing::Entity::find_addressed(auth.my_id()) .filter(auth.filter_condition()) @@ -26,7 +26,7 @@ pub async fn page( Query(page): Query, AuthIdentity(auth): AuthIdentity, ) -> crate::Result> { - let context = ctx.context_id(&id); + let context = ctx.oid(&id); crate::server::builders::paginate( url!(ctx, "/context/{id}/page"), @@ -36,6 +36,7 @@ pub async fn page( ctx.db(), page, auth.my_id(), + false, ) .await } diff --git a/src/routes/activitypub/inbox.rs b/src/routes/activitypub/inbox.rs index 1aec234..adb1eb3 100644 --- a/src/routes/activitypub/inbox.rs +++ b/src/routes/activitypub/inbox.rs @@ -20,11 +20,12 @@ pub async fn page( ) -> crate::Result> { crate::server::builders::paginate( url!(ctx, "/inbox/page"), - crate::model::addressing::Column::Actor.eq(apb::target::PUBLIC) + crate::model::addressing::Column::Actor.is_null() .into_condition(), ctx.db(), page, auth.my_id(), + false, ) .await } @@ -41,7 +42,7 @@ pub async fn post( AuthIdentity(auth): AuthIdentity, Json(activity): Json ) -> crate::Result<()> { - let Identity::Remote(server) = auth else { + let Identity::Remote { domain: server, .. } = auth else { if activity.activity_type() == Some(ActivityType::Delete) { // this is spammy af, ignore them! // we basically received a delete for a user we can't fetch and verify, meaning remote @@ -63,8 +64,7 @@ pub async fn post( return Err(UpubError::bad_request()); }; - // TODO add whitelist of relays - if !server.ends_with(&Context::server(&actor)) { + if server != Context::server(&actor) { return Err(UpubError::unauthorized()); } diff --git a/src/routes/activitypub/mod.rs b/src/routes/activitypub/mod.rs index 743b84c..808a47e 100644 --- a/src/routes/activitypub/mod.rs +++ b/src/routes/activitypub/mod.rs @@ -11,7 +11,7 @@ pub mod well_known; pub mod jsonld; pub use jsonld::JsonLD; -use axum::{http::StatusCode, response::IntoResponse, routing::{get, post, put}, Router}; +use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router}; pub trait ActivityPubRouter { fn ap_routes(self) -> Self; @@ -35,8 +35,9 @@ impl ActivityPubRouter for Router { .route("/outbox", get(ap::outbox::get)) .route("/outbox/page", get(ap::outbox::page)) // AUTH routes - .route("/auth", post(ap::auth::login)) .route("/auth", put(ap::auth::register)) + .route("/auth", post(ap::auth::login)) + .route("/auth", patch(ap::auth::refresh)) // .well-known and discovery .route("/.well-known/webfinger", get(ap::well_known::webfinger)) .route("/.well-known/host-meta", get(ap::well_known::host_meta)) @@ -44,17 +45,17 @@ impl ActivityPubRouter for Router { .route("/.well-known/oauth-authorization-server", get(ap::well_known::oauth_authorization_server)) .route("/nodeinfo/:version", get(ap::well_known::nodeinfo)) // actor routes - .route("/users/:id", get(ap::user::view)) - .route("/users/:id/inbox", post(ap::user::inbox::post)) - .route("/users/:id/inbox", get(ap::user::inbox::get)) - .route("/users/:id/inbox/page", get(ap::user::inbox::page)) - .route("/users/:id/outbox", post(ap::user::outbox::post)) - .route("/users/:id/outbox", get(ap::user::outbox::get)) - .route("/users/:id/outbox/page", get(ap::user::outbox::page)) - .route("/users/:id/followers", get(ap::user::following::get::)) - .route("/users/:id/followers/page", get(ap::user::following::page::)) - .route("/users/:id/following", get(ap::user::following::get::)) - .route("/users/:id/following/page", get(ap::user::following::page::)) + .route("/actors/:id", get(ap::user::view)) + .route("/actors/:id/inbox", post(ap::user::inbox::post)) + .route("/actors/:id/inbox", get(ap::user::inbox::get)) + .route("/actors/:id/inbox/page", get(ap::user::inbox::page)) + .route("/actors/:id/outbox", post(ap::user::outbox::post)) + .route("/actors/:id/outbox", get(ap::user::outbox::get)) + .route("/actors/:id/outbox/page", get(ap::user::outbox::page)) + .route("/actors/:id/followers", get(ap::user::following::get::)) + .route("/actors/:id/followers/page", get(ap::user::following::page::)) + .route("/actors/:id/following", get(ap::user::following::get::)) + .route("/actors/:id/following/page", get(ap::user::following::page::)) // activities .route("/activities/:id", get(ap::activity::view)) // context diff --git a/src/routes/activitypub/object/mod.rs b/src/routes/activitypub/object/mod.rs index 59a4fc9..445dd29 100644 --- a/src/routes/activitypub/object/mod.rs +++ b/src/routes/activitypub/object/mod.rs @@ -2,7 +2,7 @@ pub mod replies; use apb::{CollectionMut, ObjectMut}; use axum::extract::{Path, Query, State}; -use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns}; +use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns}; use crate::{errors::UpubError, model::{self, addressing::Event}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}}; @@ -62,7 +62,7 @@ pub async fn view( // .set_id(Some(&crate::url!(ctx, "/objects/{id}/replies"))) // .set_first(apb::Node::link(crate::url!(ctx, "/objects/{id}/replies/page"))) .set_collection_type(Some(apb::CollectionType::Collection)) - .set_total_items(Some(object.comments as u64)) + .set_total_items(Some(object.replies as u64)) .set_items(apb::Node::links(replies_ids)) ); } diff --git a/src/routes/activitypub/object/replies.rs b/src/routes/activitypub/object/replies.rs index a01ba47..b951495 100644 --- a/src/routes/activitypub/object/replies.rs +++ b/src/routes/activitypub/object/replies.rs @@ -42,6 +42,7 @@ pub async fn page( ctx.db(), page, auth.my_id(), + false, ) .await } diff --git a/src/routes/activitypub/outbox.rs b/src/routes/activitypub/outbox.rs index 1cd7e63..3abd0b3 100644 --- a/src/routes/activitypub/outbox.rs +++ b/src/routes/activitypub/outbox.rs @@ -1,4 +1,5 @@ use axum::{extract::{Query, State}, http::StatusCode, Json}; +use sea_orm::{ColumnTrait, Condition}; use crate::{errors::UpubError, routes::activitypub::{CreationResult, JsonLD, Pagination}, server::{auth::AuthIdentity, Context}, url}; @@ -13,10 +14,13 @@ pub async fn page( ) -> crate::Result> { crate::server::builders::paginate( url!(ctx, "/outbox/page"), - auth.filter_condition(), // TODO filter local only stuff + Condition::all() + .add(auth.filter_condition()) + .add(crate::model::actor::Column::Domain.eq(ctx.domain().to_string())), ctx.db(), page, auth.my_id(), + true, ) .await } diff --git a/src/routes/activitypub/user/following.rs b/src/routes/activitypub/user/following.rs index d552eb5..ae3bcb3 100644 --- a/src/routes/activitypub/user/following.rs +++ b/src/routes/activitypub/user/following.rs @@ -17,7 +17,7 @@ pub async fn get( 0 }); - crate::server::builders::collection(&url!(ctx, "/users/{id}/{follow___}"), Some(count)) + crate::server::builders::collection(&url!(ctx, "/actors/{id}/{follow___}"), Some(count)) } pub async fn page( @@ -40,7 +40,7 @@ pub async fn page( .await?; crate::server::builders::collection_page( - &url!(ctx, "/users/{id}/{follow___}/page"), + &url!(ctx, "/actors/{id}/{follow___}/page"), offset, limit, following.into_iter().map(serde_json::Value::String).collect() ) diff --git a/src/routes/activitypub/user/inbox.rs b/src/routes/activitypub/user/inbox.rs index 1d02f09..07e9849 100644 --- a/src/routes/activitypub/user/inbox.rs +++ b/src/routes/activitypub/user/inbox.rs @@ -10,9 +10,9 @@ pub async fn get( ) -> crate::Result> { match auth { Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()), - Identity::Remote(_) => Err(StatusCode::FORBIDDEN.into()), - Identity::Local(user) => if ctx.uid(&id) == user { - crate::server::builders::collection(&url!(ctx, "/users/{id}/inbox"), None) + Identity::Remote { .. } => Err(StatusCode::FORBIDDEN.into()), + Identity::Local { id: user, .. } => if ctx.uid(&id) == user { + crate::server::builders::collection(&url!(ctx, "/actors/{id}/inbox"), None) } else { Err(StatusCode::FORBIDDEN.into()) }, @@ -25,7 +25,7 @@ pub async fn page( AuthIdentity(auth): AuthIdentity, Query(page): Query, ) -> crate::Result> { - let Identity::Local(uid) = &auth else { + let Identity::Local { id: uid, internal } = &auth else { // local inbox is only for local users return Err(UpubError::forbidden()); }; @@ -34,14 +34,15 @@ pub async fn page( } crate::server::builders::paginate( - url!(ctx, "/users/{id}/inbox/page"), + url!(ctx, "/actors/{id}/inbox/page"), Condition::any() - .add(model::addressing::Column::Actor.eq(uid)) + .add(model::addressing::Column::Actor.eq(*internal)) .add(model::object::Column::AttributedTo.eq(uid)) .add(model::activity::Column::Actor.eq(uid)), ctx.db(), page, auth.my_id(), + false, ) .await } diff --git a/src/routes/activitypub/user/mod.rs b/src/routes/activitypub/user/mod.rs index 5819f24..4dfac26 100644 --- a/src/routes/activitypub/user/mod.rs +++ b/src/routes/activitypub/user/mod.rs @@ -5,10 +5,9 @@ pub mod outbox; pub mod following; use axum::extract::{Path, Query, State}; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; -use apb::{ActorMut, EndpointsMut, Node}; -use crate::{errors::UpubError, model::{self, user}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url}; +use apb::{ActorMut, EndpointsMut, Node, ObjectMut}; +use crate::{errors::UpubError, model, server::{auth::AuthIdentity, builders::AnyQuery, fetcher::Fetcher, Context}, url}; use super::{jsonld::LD, JsonLD, TryFetch}; @@ -30,48 +29,31 @@ pub async fn view( ctx.fetch_user(&uid).await?; } } + let internal_uid = model::actor::Entity::ap_to_internal(&uid, ctx.db()).await?; let (followed_by_me, following_me) = match auth.my_id() { None => (None, None), Some(my_id) => { // TODO these two queries are fast because of indexes but still are 2 subqueries for each - // user GET, not even parallelized... should really add these as joins on the main query, so + // user GET, not even parallelized... should maybe add these as joins on the main query? so // that it's one roundtrip only - let followed_by_me = model::relation::Entity::find() - .filter(model::relation::Column::Follower.eq(my_id)) - .filter(model::relation::Column::Following.eq(&uid)) - .select_only() - .select_column(model::relation::Column::Follower) - .into_tuple::() - .one(ctx.db()) - .await? - .map(|_| true); - - let following_me = model::relation::Entity::find() - .filter(model::relation::Column::Following.eq(my_id)) - .filter(model::relation::Column::Follower.eq(&uid)) - .select_only() - .select_column(model::relation::Column::Follower) - .into_tuple::() - .one(ctx.db()) - .await? - .map(|_| true); - - (followed_by_me, following_me) + let followed_by_me = model::relation::Entity::is_following(my_id, internal_uid).any(ctx.db()).await?; + let following_me = model::relation::Entity::is_following(internal_uid, my_id).any(ctx.db()).await?; + (Some(followed_by_me), Some(following_me)) }, }; - match user::Entity::find_by_id(&uid) + match model::actor::Entity::find_by_ap_id(&uid) .find_also_related(model::config::Entity) .one(ctx.db()).await? { // local user Some((user_model, Some(cfg))) => { let mut user = user_model.ap() - .set_inbox(Node::link(url!(ctx, "/users/{id}/inbox"))) - .set_outbox(Node::link(url!(ctx, "/users/{id}/outbox"))) - .set_following(Node::link(url!(ctx, "/users/{id}/following"))) - .set_followers(Node::link(url!(ctx, "/users/{id}/followers"))) + .set_inbox(Node::link(url!(ctx, "/actors/{id}/inbox"))) + .set_outbox(Node::link(url!(ctx, "/actors/{id}/outbox"))) + .set_following(Node::link(url!(ctx, "/actors/{id}/following"))) + .set_followers(Node::link(url!(ctx, "/actors/{id}/followers"))) .set_following_me(following_me) .set_followed_by_me(followed_by_me) .set_endpoints(Node::object( @@ -88,6 +70,10 @@ pub async fn view( user = user.set_following_count(None); } + if let Some(ref fe) = ctx.cfg().instance.frontend { + user = user.set_url(Node::link(format!("{fe}/actors/{id}"))); + } + Ok(JsonLD(user.ld_context())) }, // remote user diff --git a/src/routes/activitypub/user/outbox.rs b/src/routes/activitypub/user/outbox.rs index d1be16f..057d142 100644 --- a/src/routes/activitypub/user/outbox.rs +++ b/src/routes/activitypub/user/outbox.rs @@ -8,7 +8,7 @@ pub async fn get( State(ctx): State, Path(id): Path, ) -> crate::Result> { - crate::server::builders::collection(&url!(ctx, "/users/{id}/outbox"), None) + crate::server::builders::collection(&url!(ctx, "/actors/{id}/outbox"), None) } pub async fn page( @@ -19,7 +19,7 @@ pub async fn page( ) -> crate::Result> { let uid = ctx.uid(&id); crate::server::builders::paginate( - url!(ctx, "/users/{id}/outbox/page"), + url!(ctx, "/actors/{id}/outbox/page"), Condition::all() .add(auth.filter_condition()) .add( @@ -30,6 +30,7 @@ pub async fn page( ctx.db(), page, auth.my_id(), + false, ) .await } @@ -42,8 +43,8 @@ pub async fn post( ) -> Result { match auth { Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()), - Identity::Remote(_) => Err(StatusCode::NOT_IMPLEMENTED.into()), - Identity::Local(uid) => if ctx.uid(&id) == uid { + Identity::Remote { .. } => Err(StatusCode::NOT_IMPLEMENTED.into()), + Identity::Local { id: uid, .. } => if ctx.uid(&id) == uid { tracing::debug!("processing new local activity: {}", serde_json::to_string(&activity).unwrap_or_default()); match activity.base_type() { None => Err(StatusCode::BAD_REQUEST.into()), diff --git a/src/routes/activitypub/well_known.rs b/src/routes/activitypub/well_known.rs index 732f690..4c493b7 100644 --- a/src/routes/activitypub/well_known.rs +++ b/src/routes/activitypub/well_known.rs @@ -35,7 +35,7 @@ pub async fn nodeinfo_discovery(State(ctx): State) -> Json, Path(version): Path) -> Result, StatusCode> { // TODO it's unsustainable to count these every time, especially comments since it's a complex // filter! keep these numbers caches somewhere, maybe db, so that we can just look them up - let total_users = model::user::Entity::find().count(ctx.db()).await.ok(); + let total_users = model::actor::Entity::find().count(ctx.db()).await.ok(); let total_posts = None; let total_comments = None; let (software, version) = match version.as_str() { @@ -102,77 +102,37 @@ pub async fn webfinger(State(ctx): State, Query(query): Query ) -> Result, StatusCode> { - match model::user::Entity::find_by_id(ctx.uid(&id)) + match model::actor::Entity::find_by_ap_id(&ctx.uid(&id)) .find_also_related(model::config::Entity) .one(ctx.db()) .await @@ -21,7 +20,7 @@ pub async fn view( acct: x.preferred_username.clone(), avatar: x.icon.as_deref().unwrap_or("").to_string(), avatar_static: x.icon.unwrap_or_default(), - created_at: time::OffsetDateTime::from_unix_timestamp(x.created.timestamp()).unwrap(), + created_at: time::OffsetDateTime::from_unix_timestamp(x.published.timestamp()).unwrap(), display_name: x.name.unwrap_or_default(), // TODO hide these maybe followers_count: x.followers_count as u64, diff --git a/src/server/addresser.rs b/src/server/addresser.rs new file mode 100644 index 0000000..80de7b8 --- /dev/null +++ b/src/server/addresser.rs @@ -0,0 +1,130 @@ +use sea_orm::{ActiveValue::{NotSet, Set}, EntityTrait}; + +use crate::model; + +use super::{fetcher::Fetcher, Context}; + + +#[axum::async_trait] +pub trait Addresser { + async fn expand_addressing(&self, targets: Vec) -> crate::Result>; + async fn address_to(&self, aid: Option, oid: Option, targets: &[String]) -> crate::Result<()>; + async fn deliver_to(&self, aid: &str, from: &str, targets: &[String]) -> crate::Result<()>; + //#[deprecated = "should probably directly invoke address_to() since we most likely have internal ids at this point"] + async fn dispatch(&self, uid: &str, activity_targets: Vec, aid: &str, oid: Option<&str>) -> crate::Result<()>; +} + +#[axum::async_trait] +impl Addresser for super::Context { + async fn expand_addressing(&self, targets: Vec) -> crate::Result> { + let mut out = Vec::new(); + for target in targets { + if target.ends_with("/followers") { + let target_id = target.replace("/followers", ""); + let mut followers = model::relation::Entity::followers(&target_id, self.db()).await?; + if followers.is_empty() { // stuff with zero addressing will never be seen again!!! TODO + followers.push(target_id); + } + for follower in followers { + out.push(follower); + } + } else { + out.push(target); + } + } + Ok(out) + } + + async fn address_to(&self, aid: Option, oid: Option, targets: &[String]) -> crate::Result<()> { + // TODO address_to became kind of expensive, with these two selects right away and then another + // select for each target we're addressing to... can this be improved?? + let local_activity = if let Some(x) = aid { self.is_local_internal_activity(x).await.unwrap_or(false) } else { false }; + let local_object = if let Some(x) = oid { self.is_local_internal_object(x).await.unwrap_or(false) } else { false }; + let mut addressing = Vec::new(); + for target in targets + .iter() + .filter(|to| !to.is_empty()) + .filter(|to| !to.ends_with("/followers")) + .filter(|to| local_activity || local_object || to.as_str() == apb::target::PUBLIC || self.is_local(to)) + { + let (server, actor) = if target == apb::target::PUBLIC { (None, None) } else { + match ( + model::instance::Entity::domain_to_internal(&Context::server(target), self.db()).await, + model::actor::Entity::ap_to_internal(target, self.db()).await, + ) { + (Ok(server), Ok(actor)) => (Some(server), Some(actor)), + (Err(e), Ok(_)) => { tracing::error!("failed resolving domain: {e}"); continue; }, + (Ok(_), Err(e)) => { tracing::error!("failed resolving actor: {e}"); continue; }, + (Err(es), Err(ea)) => { tracing::error!("failed resolving domain ({es}) and actor ({ea})"); continue; }, + } + }; + addressing.push( + model::addressing::ActiveModel { + internal: NotSet, + instance: Set(server), + actor: Set(actor), + activity: Set(aid), + object: Set(oid), + published: Set(chrono::Utc::now()), + } + ); + } + + if !addressing.is_empty() { + model::addressing::Entity::insert_many(addressing) + .exec(self.db()) + .await?; + } + + Ok(()) + } + + async fn deliver_to(&self, aid: &str, from: &str, targets: &[String]) -> crate::Result<()> { + let mut deliveries = Vec::new(); + for target in targets.iter() + .filter(|to| !to.is_empty()) + .filter(|to| Context::server(to) != self.domain()) + .filter(|to| to != &apb::target::PUBLIC) + { + // TODO fetch concurrently + match self.fetch_user(target).await { + Ok(model::actor::Model { inbox: Some(inbox), .. }) => deliveries.push( + model::delivery::ActiveModel { + internal: sea_orm::ActiveValue::NotSet, + actor: Set(from.to_string()), + // TODO we should resolve each user by id and check its inbox because we can't assume + // it's /actors/{id}/inbox for every software, but oh well it's waaaaay easier now + target: Set(inbox), + activity: Set(aid.to_string()), + published: Set(chrono::Utc::now()), + not_before: Set(chrono::Utc::now()), + attempt: Set(0), + } + ), + Ok(_) => tracing::error!("resolved target but missing inbox: '{target}', skipping delivery"), + Err(e) => tracing::error!("failed resolving target inbox: {e}, skipping delivery to '{target}'"), + } + } + + if !deliveries.is_empty() { + model::delivery::Entity::insert_many(deliveries) + .exec(self.db()) + .await?; + } + + self.dispatcher().wakeup(); + + Ok(()) + } + + //#[deprecated = "should probably directly invoke address_to() since we most likely have internal ids at this point"] + async fn dispatch(&self, uid: &str, activity_targets: Vec, aid: &str, oid: Option<&str>) -> crate::Result<()> { + let addressed = self.expand_addressing(activity_targets).await?; + let internal_aid = model::activity::Entity::ap_to_internal(aid, self.db()).await?; + let internal_oid = if let Some(o) = oid { Some(model::object::Entity::ap_to_internal(o, self.db()).await?) } else { None }; + self.address_to(Some(internal_aid), internal_oid, &addressed).await?; + self.deliver_to(aid, uid, &addressed).await?; + Ok(()) + } + +} diff --git a/src/server/admin.rs b/src/server/admin.rs index 0ee1dd7..bd7c2ba 100644 --- a/src/server/admin.rs +++ b/src/server/admin.rs @@ -1,4 +1,4 @@ -use sea_orm::{EntityTrait, IntoActiveModel}; +use sea_orm::{ActiveValue::{Set, NotSet}, EntityTrait}; #[axum::async_trait] pub trait Administrable { @@ -28,52 +28,56 @@ impl Administrable for super::Context { let ap_id = self.uid(&username); let db = self.db(); let domain = self.domain().to_string(); - let user_model = crate::model::user::Model { - id: ap_id.clone(), - name: display_name, - domain, summary, - preferred_username: username.clone(), - following: None, - following_count: 0, - followers: None, - followers_count: 0, - statuses_count: 0, - icon: avatar_url, - image: banner_url, - inbox: None, - shared_inbox: None, - outbox: None, - actor_type: apb::ActorType::Person, - created: chrono::Utc::now(), - updated: chrono::Utc::now(), - private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()), - public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(), + let user_model = crate::model::actor::ActiveModel { + internal: NotSet, + id: Set(ap_id.clone()), + name: Set(display_name), + domain: Set(domain), + summary: Set(summary), + preferred_username: Set(username.clone()), + following: Set(None), + following_count: Set(0), + followers: Set(None), + followers_count: Set(0), + statuses_count: Set(0), + icon: Set(avatar_url), + image: Set(banner_url), + inbox: Set(None), + shared_inbox: Set(None), + outbox: Set(None), + actor_type: Set(apb::ActorType::Person), + published: Set(chrono::Utc::now()), + updated: Set(chrono::Utc::now()), + private_key: Set(Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string())), + public_key: Set(std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string()), }; - crate::model::user::Entity::insert(user_model.into_active_model()) + crate::model::actor::Entity::insert(user_model) .exec(db) .await?; - let config_model = crate::model::config::Model { - id: ap_id.clone(), - accept_follow_requests: true, - show_followers_count: true, - show_following_count: true, - show_followers: false, - show_following: false, + let config_model = crate::model::config::ActiveModel { + internal: NotSet, + actor: Set(ap_id.clone()), + accept_follow_requests: Set(true), + show_followers_count: Set(true), + show_following_count: Set(true), + show_followers: Set(false), + show_following: Set(false), }; - crate::model::config::Entity::insert(config_model.into_active_model()) + crate::model::config::Entity::insert(config_model) .exec(db) .await?; - let credentials_model = crate::model::credential::Model { - id: ap_id, - email: username, - password, + let credentials_model = crate::model::credential::ActiveModel { + internal: NotSet, + actor: Set(ap_id), + login: Set(username), + password: Set(sha256::digest(password)), }; - crate::model::credential::Entity::insert(credentials_model.into_active_model()) + crate::model::credential::Entity::insert(credentials_model) .exec(db) .await?; diff --git a/src/server/auth.rs b/src/server/auth.rs index 2470e86..1516bbc 100644 --- a/src/server/auth.rs +++ b/src/server/auth.rs @@ -9,63 +9,58 @@ use super::{fetcher::Fetcher, httpsign::HttpSignature}; #[derive(Debug, Clone)] pub enum Identity { Anonymous, - Local(String), - Remote(String), + Remote { + domain: String, + internal: i64, + }, + Local { + id: String, + internal: i64, + }, } impl Identity { pub fn filter_condition(&self) -> Condition { - let base_cond = Condition::any().add(model::addressing::Column::Actor.eq(apb::target::PUBLIC)); + let base_cond = Condition::any().add(model::addressing::Column::Actor.is_null()); match self { Identity::Anonymous => base_cond, - Identity::Remote(server) => base_cond.add(model::addressing::Column::Server.eq(server)), + Identity::Remote { internal, .. } => base_cond.add(model::addressing::Column::Instance.eq(*internal)), // TODO should we allow all users on same server to see? or just specific user?? - Identity::Local(uid) => base_cond - .add(model::addressing::Column::Actor.eq(uid)) - .add(model::activity::Column::Actor.eq(uid)) - .add(model::object::Column::AttributedTo.eq(uid)), + Identity::Local { id, internal } => base_cond + .add(model::addressing::Column::Actor.eq(*internal)) + .add(model::activity::Column::Actor.eq(id)) + .add(model::object::Column::AttributedTo.eq(id)), } } - pub fn my_id(&self) -> Option<&str> { + pub fn my_id(&self) -> Option { match self { - Identity::Local(x) => Some(x.as_str()), + Identity::Local { internal, .. } => Some(*internal), _ => None, } } - pub fn is(&self, id: &str) -> bool { + pub fn is(&self, uid: &str) -> bool { match self { Identity::Anonymous => false, - Identity::Remote(_) => false, // TODO per-actor server auth should check this - Identity::Local(uid) => uid.as_str() == id + Identity::Remote { .. } => false, // TODO per-actor server auth should check this + Identity::Local { id, .. } => id.as_str() == uid } } + #[allow(unused)] pub fn is_anon(&self) -> bool { matches!(self, Self::Anonymous) } + #[allow(unused)] pub fn is_local(&self) -> bool { - matches!(self, Self::Local(_)) + matches!(self, Self::Local { .. }) } + #[allow(unused)] pub fn is_remote(&self) -> bool { - matches!(self, Self::Remote(_)) - } - - pub fn is_local_user(&self, uid: &str) -> bool { - match self { - Self::Local(x) => x == uid, - _ => false, - } - } - - pub fn is_remote_server(&self, uid: &str) -> bool { - match self { - Self::Remote(x) => x == uid, - _ => false, - } + matches!(self, Self::Remote { .. }) } } @@ -90,13 +85,19 @@ where .unwrap_or(""); if auth_header.starts_with("Bearer ") { - match model::session::Entity::find_by_id(auth_header.replace("Bearer ", "")) + match model::session::Entity::find() + .filter(model::session::Column::Secret.eq(auth_header.replace("Bearer ", ""))) .filter(model::session::Column::Expires.gt(chrono::Utc::now())) .one(ctx.db()) .await { - Ok(Some(x)) => identity = Identity::Local(x.actor), Ok(None) => return Err(UpubError::unauthorized()), + Ok(Some(x)) => { + // TODO could we store both actor ap id and internal id in session? to avoid this extra + // lookup on *every* local authed request we receive... + let internal = model::actor::Entity::ap_to_internal(&x.actor, ctx.db()).await?; + identity = Identity::Local { id: x.actor, internal }; + }, Err(e) => { tracing::error!("failed querying user session: {e}"); return Err(UpubError::internal_server_error()) @@ -122,7 +123,13 @@ where .build_from_parts(parts) .verify(&user.public_key) { - Ok(true) => identity = Identity::Remote(Context::server(&user_id)), + Ok(true) => { + // TODO can we avoid this extra db rountrip made on each server fetch? + let domain = Context::server(&user_id); + // TODO this will fail because we never fetch and insert into instance oops + let internal = model::instance::Entity::domain_to_internal(&domain, ctx.db()).await?; + identity = Identity::Remote { domain, internal }; + }, Ok(false) => tracing::warn!("invalid signature: {http_signature:?}"), Err(e) => tracing::error!("error verifying signature: {e}"), }, diff --git a/src/server/builders.rs b/src/server/builders.rs index 7664d63..0f8b641 100644 --- a/src/server/builders.rs +++ b/src/server/builders.rs @@ -1,5 +1,5 @@ use apb::{BaseMut, CollectionMut, CollectionPageMut}; -use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect}; +use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect, RelationTrait}; use crate::{model::{addressing::Event, attachment::BatchFillable}, routes::activitypub::{jsonld::LD, JsonLD, Pagination}}; @@ -8,12 +8,20 @@ pub async fn paginate( filter: Condition, db: &DatabaseConnection, page: Pagination, - my_id: Option<&str>, + my_id: Option, + with_users: bool, // TODO ewww too many arguments for this weird function... ) -> crate::Result> { let limit = page.batch.unwrap_or(20).min(50); let offset = page.offset.unwrap_or(0); - let items = crate::model::addressing::Entity::find_addressed(my_id) + let mut select = crate::model::addressing::Entity::find_addressed(my_id); + + if with_users { + select = select + .join(sea_orm::JoinType::InnerJoin, crate::model::activity::Relation::Actors.def()); + } + + let items = select .filter(filter) // TODO also limit to only local activities .limit(limit) @@ -27,7 +35,7 @@ pub async fn paginate( let items : Vec = items .into_iter() .map(|item| { - let attach = attachments.remove(item.id()); + let attach = attachments.remove(&item.internal()); item.ap(attach) }) .collect(); @@ -63,3 +71,22 @@ pub fn collection(id: &str, total_items: Option) -> crate::Result crate::Result; +} + +#[axum::async_trait] +impl AnyQuery for sea_orm::Select { + async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result { + Ok(self.one(db).await?.is_some()) + } +} + +#[axum::async_trait] +impl AnyQuery for sea_orm::Selector { + async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result { + Ok(self.one(db).await?.is_some()) + } +} diff --git a/src/server/context.rs b/src/server/context.rs index 2021622..7e1796f 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -1,12 +1,11 @@ use std::{collections::BTreeSet, sync::Arc}; -use openssl::rsa::Rsa; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; -use crate::{config::Config, model, server::fetcher::Fetcher}; +use crate::{config::Config, errors::UpubError, model}; use uriproxy::UriClass; -use super::dispatcher::Dispatcher; +use super::{builders::AnyQuery, dispatcher::Dispatcher}; #[derive(Clone)] @@ -19,8 +18,16 @@ struct ContextInner { base_url: String, dispatcher: Dispatcher, // TODO keep these pre-parsed - app: model::application::Model, - relays: BTreeSet, + actor: model::actor::Model, + instance: model::instance::Model, + pkey: String, + #[allow(unused)] relay: Relays, +} + +#[allow(unused)] +pub struct Relays { + sources: BTreeSet, + sinks: BTreeSet, } #[macro_export] @@ -46,42 +53,37 @@ impl Context { for _ in 0..1 { // TODO customize delivery workers amount dispatcher.spawn(db.clone(), domain.clone(), 30); // TODO ew don't do it this deep and secretly!! } - let app = match model::application::Entity::find().one(&db).await? { - Some(model) => model, - None => { - tracing::info!("generating application keys"); - let rsa = Rsa::generate(2048)?; - let privk = std::str::from_utf8(&rsa.private_key_to_pem()?)?.to_string(); - let pubk = std::str::from_utf8(&rsa.public_key_to_pem()?)?.to_string(); - let system = model::application::ActiveModel { - id: sea_orm::ActiveValue::NotSet, - private_key: sea_orm::ActiveValue::Set(privk.clone()), - public_key: sea_orm::ActiveValue::Set(pubk.clone()), - created: sea_orm::ActiveValue::Set(chrono::Utc::now()), - }; - model::application::Entity::insert(system).exec(&db).await?; - // sqlite doesn't resurn last inserted id so we're better off just querying again, it's just one time - model::application::Entity::find().one(&db).await?.expect("could not find app config just inserted") - } + let base_url = format!("{}{}", protocol, domain); + + let (actor, instance) = super::init::application(domain.clone(), base_url.clone(), &db).await?; + + // TODO maybe we could provide a more descriptive error... + let pkey = actor.private_key.as_deref().ok_or_else(UpubError::internal_server_error)?.to_string(); + + let relay_sinks = model::relation::Entity::followers(&actor.id, &db).await?; + let relay_sources = model::relation::Entity::following(&actor.id, &db).await?; + + let relay = Relays { + sources: BTreeSet::from_iter(relay_sources), + sinks: BTreeSet::from_iter(relay_sinks), }; - let relays = model::relay::Entity::find() - .select_only() - .select_column(model::relay::Column::Id) - .filter(model::relay::Column::Accepted.eq(true)) - .into_tuple::() - .all(&db) - .await?; - Ok(Context(Arc::new(ContextInner { - base_url: format!("{}{}", protocol, domain), - db, domain, protocol, app, dispatcher, config, - relays: BTreeSet::from_iter(relays.into_iter()), + base_url, db, domain, protocol, actor, instance, dispatcher, config, pkey, relay, }))) } - pub fn app(&self) -> &model::application::Model { - &self.0.app + pub fn actor(&self) -> &model::actor::Model { + &self.0.actor + } + + #[allow(unused)] + pub fn instance(&self) -> &model::instance::Model { + &self.0.instance + } + + pub fn pkey(&self) -> &str { + &self.0.pkey } pub fn db(&self) -> &DatabaseConnection { @@ -104,9 +106,13 @@ impl Context { &self.0.base_url } + pub fn dispatcher(&self) -> &Dispatcher { + &self.0.dispatcher + } + /// get full user id uri pub fn uid(&self, id: &str) -> String { - uriproxy::uri(self.base(), UriClass::User, id) + uriproxy::uri(self.base(), UriClass::Actor, id) } /// get full object id uri @@ -119,14 +125,6 @@ impl Context { uriproxy::uri(self.base(), UriClass::Activity, id) } - // TODO remove this!! - pub fn context_id(&self, id: &str) -> String { - if id.starts_with("tag:") { - return id.to_string(); - } - uriproxy::uri(self.base(), UriClass::Context, id) - } - /// get bare id, which is uuid for local stuff and +{uri|base64} for remote stuff pub fn id(&self, full_id: &str) -> String { if self.is_local(full_id) { @@ -150,100 +148,39 @@ impl Context { id.starts_with(self.base()) } - pub async fn expand_addressing(&self, targets: Vec) -> crate::Result> { - let mut out = Vec::new(); - for target in targets { - if target.ends_with("/followers") { - let target_id = target.replace("/followers", ""); - model::relation::Entity::find() - .filter(model::relation::Column::Following.eq(target_id)) - .select_only() - .select_column(model::relation::Column::Follower) - .into_tuple::() - .all(self.db()) - .await? - .into_iter() - .for_each(|x| out.push(x)); - } else { - out.push(target); - } - } - Ok(out) + pub async fn is_local_internal_object(&self, internal: i64) -> crate::Result { + model::object::Entity::find() + .filter(model::object::Column::Internal.eq(internal)) + .select_only() + .select_column(model::object::Column::Internal) + .into_tuple::() + .any(self.db()) + .await } - pub async fn address_to(&self, aid: Option<&str>, oid: Option<&str>, targets: &[String]) -> crate::Result<()> { - let local_activity = aid.map(|x| self.is_local(x)).unwrap_or(false); - let local_object = oid.map(|x| self.is_local(x)).unwrap_or(false); - let addressings : Vec = targets - .iter() - .filter(|to| !to.is_empty()) - .filter(|to| !to.ends_with("/followers")) - .filter(|to| local_activity || local_object || to.as_str() == apb::target::PUBLIC || self.is_local(to)) - .map(|to| model::addressing::ActiveModel { - id: sea_orm::ActiveValue::NotSet, - server: Set(Context::server(to)), - actor: Set(to.to_string()), - activity: Set(aid.map(|x| x.to_string())), - object: Set(oid.map(|x| x.to_string())), - published: Set(chrono::Utc::now()), - }) - .collect(); - - if !addressings.is_empty() { - model::addressing::Entity::insert_many(addressings) - .exec(self.db()) - .await?; - } - - Ok(()) + pub async fn is_local_internal_activity(&self, internal: i64) -> crate::Result { + model::activity::Entity::find() + .filter(model::activity::Column::Internal.eq(internal)) + .select_only() + .select_column(model::activity::Column::Internal) + .into_tuple::() + .any(self.db()) + .await } - pub async fn deliver_to(&self, aid: &str, from: &str, targets: &[String]) -> crate::Result<()> { - let mut deliveries = Vec::new(); - for target in targets.iter() - .filter(|to| !to.is_empty()) - .filter(|to| Context::server(to) != self.domain()) - .filter(|to| to != &apb::target::PUBLIC) - { - // TODO fetch concurrently - match self.fetch_user(target).await { - Ok(model::user::Model { inbox: Some(inbox), .. }) => deliveries.push( - model::delivery::ActiveModel { - id: sea_orm::ActiveValue::NotSet, - actor: Set(from.to_string()), - // TODO we should resolve each user by id and check its inbox because we can't assume - // it's /users/{id}/inbox for every software, but oh well it's waaaaay easier now - target: Set(inbox), - activity: Set(aid.to_string()), - created: Set(chrono::Utc::now()), - not_before: Set(chrono::Utc::now()), - attempt: Set(0), - } - ), - Ok(_) => tracing::error!("resolved target but missing inbox: '{target}', skipping delivery"), - Err(e) => tracing::error!("failed resolving target inbox: {e}, skipping delivery to '{target}'"), - } - } - - if !deliveries.is_empty() { - model::delivery::Entity::insert_many(deliveries) - .exec(self.db()) - .await?; - } - - self.0.dispatcher.wakeup(); - - Ok(()) - } - - pub async fn dispatch(&self, uid: &str, activity_targets: Vec, aid: &str, oid: Option<&str>) -> crate::Result<()> { - let addressed = self.expand_addressing(activity_targets).await?; - self.address_to(Some(aid), oid, &addressed).await?; - self.deliver_to(aid, uid, &addressed).await?; - Ok(()) + #[allow(unused)] + pub async fn is_local_internal_actor(&self, internal: i64) -> crate::Result { + model::actor::Entity::find() + .filter(model::actor::Column::Internal.eq(internal)) + .select_only() + .select_column(model::actor::Column::Internal) + .into_tuple::() + .any(self.db()) + .await } + #[allow(unused)] pub fn is_relay(&self, id: &str) -> bool { - self.0.relays.contains(id) + self.0.relay.sources.contains(id) || self.0.relay.sinks.contains(id) } } diff --git a/src/server/dispatcher.rs b/src/server/dispatcher.rs index 4a54550..e24620c 100644 --- a/src/server/dispatcher.rs +++ b/src/server/dispatcher.rs @@ -54,7 +54,7 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker }; let del_row = model::delivery::ActiveModel { - id: sea_orm::ActiveValue::Set(delivery.id), + internal: sea_orm::ActiveValue::Set(delivery.internal), ..Default::default() }; let del = model::delivery::Entity::delete(del_row) @@ -72,7 +72,7 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker tracing::info!("delivering {} to {}", delivery.activity, delivery.target); - let payload = match model::activity::Entity::find_by_id(&delivery.activity) + let payload = match model::activity::Entity::find_by_ap_id(&delivery.activity) .find_also_related(model::object::Entity) .one(db) .await? // TODO probably should not fail here and at least re-insert the delivery @@ -99,24 +99,19 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker }, }; - let key = if delivery.actor == format!("https://{domain}") { - let Some(model::application::Model { private_key: key, .. }) = model::application::Entity::find() - .one(db).await? - else { - tracing::error!("no private key configured for application"); - continue; - }; - key - } else { - let Some(model::user::Model{ private_key: Some(key), .. }) = model::user::Entity::find_by_id(&delivery.actor) - .one(db).await? - else { - tracing::error!("can not dispatch activity for user without private key: {}", delivery.actor); - continue; - }; - key + let Some(actor) = model::actor::Entity::find_by_ap_id(&delivery.actor) + .one(db) + .await? + else { + tracing::error!("abandoning delivery of {} from non existant actor: {}", delivery.activity, delivery.actor); + continue; }; + let Some(key) = actor.private_key + else { + tracing::error!("abandoning delivery of {} from actor without private key: {}", delivery.activity, delivery.actor); + continue; + }; if let Err(e) = Context::request( Method::POST, &delivery.target, @@ -125,12 +120,12 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker ).await { tracing::warn!("failed delivery of {} to {} : {e}", delivery.activity, delivery.target); let new_delivery = model::delivery::ActiveModel { - id: sea_orm::ActiveValue::NotSet, + internal: sea_orm::ActiveValue::NotSet, not_before: sea_orm::ActiveValue::Set(delivery.next_delivery()), actor: sea_orm::ActiveValue::Set(delivery.actor), target: sea_orm::ActiveValue::Set(delivery.target), activity: sea_orm::ActiveValue::Set(delivery.activity), - created: sea_orm::ActiveValue::Set(delivery.created), + published: sea_orm::ActiveValue::Set(delivery.published), attempt: sea_orm::ActiveValue::Set(delivery.attempt + 1), }; model::delivery::Entity::insert(new_delivery).exec(db).await?; diff --git a/src/server/fetcher.rs b/src/server/fetcher.rs index 44830ff..1460aa5 100644 --- a/src/server/fetcher.rs +++ b/src/server/fetcher.rs @@ -1,26 +1,69 @@ use std::collections::BTreeMap; -use apb::{target::Addressed, Activity, Base, Collection, CollectionPage, Link, Object}; +use apb::{target::Addressed, Activity, Actor, ActorMut, Base, Collection, Object}; use base64::Engine; use reqwest::{header::{ACCEPT, CONTENT_TYPE, USER_AGENT}, Method, Response}; -use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; +use sea_orm::{EntityTrait, IntoActiveModel, NotSet}; use crate::{errors::UpubError, model, VERSION}; -use super::{httpsign::HttpSignature, normalizer::Normalizer, Context}; +use super::{addresser::Addresser, httpsign::HttpSignature, normalizer::Normalizer, Context}; + +#[derive(Debug, Clone)] +pub enum PullResult { + Actor(T), + Activity(T), + Object(T), +} + +impl PullResult { + pub fn actor(self) -> crate::Result { + match self { + Self::Actor(x) => Ok(x), + Self::Activity(x) => Err(UpubError::Mismatch(apb::ObjectType::Actor(apb::ActorType::Person), x.object_type().unwrap_or(apb::ObjectType::Activity(apb::ActivityType::Activity)))), + Self::Object(x) => Err(UpubError::Mismatch(apb::ObjectType::Actor(apb::ActorType::Person), x.object_type().unwrap_or(apb::ObjectType::Object))), + } + } + + pub fn activity(self) -> crate::Result { + match self { + Self::Actor(x) => Err(UpubError::Mismatch(apb::ObjectType::Activity(apb::ActivityType::Activity), x.object_type().unwrap_or(apb::ObjectType::Actor(apb::ActorType::Person)))), + Self::Activity(x) => Ok(x), + Self::Object(x) => Err(UpubError::Mismatch(apb::ObjectType::Activity(apb::ActivityType::Activity), x.object_type().unwrap_or(apb::ObjectType::Object))), + } + } + + pub fn object(self) -> crate::Result { + match self { + Self::Actor(x) => Err(UpubError::Mismatch(apb::ObjectType::Object, x.object_type().unwrap_or(apb::ObjectType::Actor(apb::ActorType::Person)))), + Self::Activity(x) => Err(UpubError::Mismatch(apb::ObjectType::Object, x.object_type().unwrap_or(apb::ObjectType::Activity(apb::ActivityType::Activity)))), + Self::Object(x) => Ok(x), + } + } +} #[axum::async_trait] pub trait Fetcher { + async fn pull(&self, id: &str) -> crate::Result> { self.pull_r(id, 0).await } + async fn pull_r(&self, id: &str, depth: u32) -> crate::Result>; + + async fn webfinger(&self, user: &str, host: &str) -> crate::Result; - async fn fetch_user(&self, id: &str) -> crate::Result; - async fn pull_user(&self, id: &str) -> crate::Result; + async fn fetch_domain(&self, domain: &str) -> crate::Result; - async fn fetch_object(&self, id: &str) -> crate::Result; - async fn pull_object(&self, id: &str) -> crate::Result; + async fn fetch_user(&self, id: &str) -> crate::Result; + async fn resolve_user(&self, actor: serde_json::Value) -> crate::Result; async fn fetch_activity(&self, id: &str) -> crate::Result; - async fn pull_activity(&self, id: &str) -> crate::Result; + async fn resolve_activity(&self, activity: serde_json::Value) -> crate::Result; + + async fn fetch_object(&self, id: &str) -> crate::Result { self.fetch_object_r(id, 0).await } + #[allow(unused)] async fn resolve_object(&self, object: serde_json::Value) -> crate::Result { self.resolve_object_r(object, 0).await } + + async fn fetch_object_r(&self, id: &str, depth: u32) -> crate::Result; + async fn resolve_object_r(&self, object: serde_json::Value, depth: u32) -> crate::Result; + async fn fetch_thread(&self, id: &str) -> crate::Result<()>; @@ -83,6 +126,36 @@ pub trait Fetcher { #[axum::async_trait] impl Fetcher for Context { + async fn pull_r(&self, id: &str, depth: u32) -> crate::Result> { + let _domain = self.fetch_domain(&Context::server(id)).await?; + + let document = Self::request( + Method::GET, id, None, + &format!("https://{}", self.domain()), self.pkey(), self.domain(), + ) + .await? + .json::() + .await?; + + let doc_id = document.id().ok_or_else(|| UpubError::field("id"))?; + if id != doc_id { + if depth >= self.cfg().security.max_id_redirects { + return Err(UpubError::unprocessable()); + } + return self.pull(doc_id).await; + } + + match document.object_type() { + None => Err(UpubError::bad_request()), + Some(apb::ObjectType::Collection(_)) => Err(UpubError::unprocessable()), + Some(apb::ObjectType::Tombstone) => Err(UpubError::not_found()), + Some(apb::ObjectType::Activity(_)) => Ok(PullResult::Activity(document)), + Some(apb::ObjectType::Actor(_)) => Ok(PullResult::Actor(document)), + _ => Ok(PullResult::Object(document)), + } + } + + async fn webfinger(&self, user: &str, host: &str) -> crate::Result { let subject = format!("acct:{user}@{host}"); let webfinger_uri = format!("https://{host}/.well-known/webfinger?resource={subject}"); @@ -114,82 +187,125 @@ impl Fetcher for Context { Err(UpubError::not_found()) } - - async fn fetch_user(&self, id: &str) -> crate::Result { - if let Some(x) = model::user::Entity::find_by_id(id).one(self.db()).await? { + async fn fetch_domain(&self, domain: &str) -> crate::Result { + if let Some(x) = model::instance::Entity::find_by_domain(domain).one(self.db()).await? { return Ok(x); // already in db, easy } - let user_model = self.pull_user(id).await?; + let mut instance_model = model::instance::Model { + internal: 0, + domain: domain.to_string(), + name: None, + software: None, + down_since: None, + icon: None, + version: None, + users: None, + posts: None, + published: chrono::Utc::now(), + updated: chrono::Utc::now(), + }; - // TODO this may fail: while fetching, remote server may fetch our service actor. - // if it does so with http signature, we will fetch that actor in background - // meaning that, once we reach here, it's already inserted and returns an UNIQUE error - model::user::Entity::insert(user_model.clone().into_active_model()) - .exec(self.db()).await?; + if let Ok(res) = Self::request( + Method::GET, &format!("https://{domain}"), None, &format!("https://{}", self.domain()), self.pkey(), self.domain(), + ).await { + if let Ok(actor) = res.json::().await { + if let Some(name) = actor.name() { + instance_model.name = Some(name.to_string()); + } + if let Some(icon) = actor.icon().id() { + instance_model.icon = Some(icon); + } + } + } - Ok(user_model) + if let Ok(nodeinfo) = model::instance::Entity::nodeinfo(domain).await { + instance_model.software = Some(nodeinfo.software.name); + instance_model.version = nodeinfo.software.version; + instance_model.users = nodeinfo.usage.users.and_then(|x| x.total); + instance_model.posts = nodeinfo.usage.local_posts; + } + + let mut active_model = instance_model.clone().into_active_model(); + active_model.internal = NotSet; + model::instance::Entity::insert(active_model).exec(self.db()).await?; + + let internal = model::instance::Entity::domain_to_internal(domain, self.db()).await?; + instance_model.internal = internal; + + Ok(instance_model) } - async fn pull_user(&self, id: &str) -> crate::Result { - let user = Self::request( - Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(), - ).await?.json::().await?; - let mut user_model = model::user::Model::new(&user)?; + async fn resolve_user(&self, mut document: serde_json::Value) -> crate::Result { + let id = document.id().ok_or_else(|| UpubError::field("id"))?.to_string(); - // TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs - if let Some(followers_url) = &user_model.followers { + // TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs every time + if let Some(followers_url) = &document.followers().id() { let req = Self::request( Method::GET, followers_url, None, - &format!("https://{}", self.domain()), &self.app().private_key, self.domain(), + &format!("https://{}", self.domain()), self.pkey(), self.domain(), ).await; if let Ok(res) = req { if let Ok(user_followers) = res.json::().await { if let Some(total) = user_followers.total_items() { - user_model.followers_count = total as i64; + document = document.set_followers_count(Some(total)); } } } } - if let Some(following_url) = &user_model.following { + if let Some(following_url) = &document.following().id() { let req = Self::request( Method::GET, following_url, None, - &format!("https://{}", self.domain()), &self.app().private_key, self.domain(), + &format!("https://{}", self.domain()), self.pkey(), self.domain(), ).await; if let Ok(res) = req { if let Ok(user_following) = res.json::().await { if let Some(total) = user_following.total_items() { - user_model.following_count = total as i64; + document = document.set_following_count(Some(total)); } } } } - Ok(user_model) + let user_model = model::actor::ActiveModel::new(&document)?; + + // TODO this may fail: while fetching, remote server may fetch our service actor. + // if it does so with http signature, we will fetch that actor in background + // meaning that, once we reach here, it's already inserted and returns an UNIQUE error + model::actor::Entity::insert(user_model).exec(self.db()).await?; + + // TODO fetch it back to get the internal id + Ok( + model::actor::Entity::find_by_ap_id(&id) + .one(self.db()) + .await? + .ok_or_else(UpubError::internal_server_error)? + ) } - async fn fetch_activity(&self, id: &str) -> crate::Result { - if let Some(x) = model::activity::Entity::find_by_id(id).one(self.db()).await? { + async fn fetch_user(&self, id: &str) -> crate::Result { + if let Some(x) = model::actor::Entity::find_by_ap_id(id).one(self.db()).await? { return Ok(x); // already in db, easy } - let activity_model = self.pull_activity(id).await?; + let document = self.pull(id).await?.actor()?; - model::activity::Entity::insert(activity_model.clone().into_active_model()) - .exec(self.db()).await?; - - let addressed = activity_model.addressed(); - let expanded_addresses = self.expand_addressing(addressed).await?; - self.address_to(Some(&activity_model.id), None, &expanded_addresses).await?; - - Ok(activity_model) + self.resolve_user(document).await } - async fn pull_activity(&self, id: &str) -> crate::Result { - let activity = Self::request( - Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(), - ).await?.json::().await?; + async fn fetch_activity(&self, id: &str) -> crate::Result { + if let Some(x) = model::activity::Entity::find_by_ap_id(id).one(self.db()).await? { + return Ok(x); // already in db, easy + } + + let activity = self.pull(id).await?.activity()?; + + self.resolve_activity(activity).await + } + + async fn resolve_activity(&self, activity: serde_json::Value) -> crate::Result { + let id = activity.id().ok_or_else(|| UpubError::field("id"))?.to_string(); if let Some(activity_actor) = activity.actor().id() { if let Err(e) = self.fetch_user(&activity_actor).await { @@ -203,121 +319,64 @@ impl Fetcher for Context { } } - let activity_model = model::activity::Model::new(&activity)?; + let activity_model = self.insert_activity(activity, Some(Context::server(&id))).await?; + + let addressed = activity_model.addressed(); + let expanded_addresses = self.expand_addressing(addressed).await?; + self.address_to(Some(activity_model.internal), None, &expanded_addresses).await?; Ok(activity_model) } - async fn fetch_thread(&self, id: &str) -> crate::Result<()> { - crawl_replies(self, id, 0).await + async fn fetch_thread(&self, _id: &str) -> crate::Result<()> { + // crawl_replies(self, id, 0).await + todo!() } - async fn fetch_object(&self, id: &str) -> crate::Result { - fetch_object_inner(self, id, 0).await - } - - async fn pull_object(&self, id: &str) -> crate::Result { - let object = Context::request( - Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(), - ).await?.json::().await?; - - Ok(model::object::Model::new(&object)?) - } -} - -#[async_recursion::async_recursion] -async fn crawl_replies(ctx: &Context, id: &str, depth: usize) -> crate::Result<()> { - tracing::info!("crawling replies of '{id}'"); - let object = Context::request( - Method::GET, id, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), - ).await?.json::().await?; - - let object_model = model::object::Model::new(&object)?; - match model::object::Entity::insert(object_model.into_active_model()) - .exec(ctx.db()).await - { - Ok(_) => {}, - Err(sea_orm::DbErr::RecordNotInserted) => {}, - Err(sea_orm::DbErr::Exec(_)) => {}, // ughhh bad fix for sqlite - Err(e) => return Err(e.into()), - } - - if depth > 16 { - tracing::warn!("stopping thread crawling: too deep!"); - return Ok(()); - } - - let mut page_url = match object.replies().get() { - Some(serde_json::Value::String(x)) => { - let replies = Context::request( - Method::GET, x, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), - ).await?.json::().await?; - replies.first().id() - }, - Some(serde_json::Value::Object(x)) => { - let obj = serde_json::Value::Object(x.clone()); // lol putting it back, TODO! - obj.first().id() - }, - _ => return Ok(()), - }; - - while let Some(ref url) = page_url { - let replies = Context::request( - Method::GET, url, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), - ).await?.json::().await?; - - for reply in replies.items() { - // TODO right now it crawls one by one, could be made in parallel but would be quite more - // abusive, so i'll keep it like this while i try it out - crawl_replies(ctx, reply.href(), depth + 1).await?; + async fn fetch_object_r(&self, id: &str, depth: u32) -> crate::Result { + if let Some(x) = model::object::Entity::find_by_ap_id(id).one(self.db()).await? { + return Ok(x); // already in db, easy } - page_url = replies.next().id(); + let object = self.pull(id).await?.object()?; + + self.resolve_object_r(object, depth).await } - Ok(()) -} + async fn resolve_object_r(&self, object: serde_json::Value, depth: u32) -> crate::Result { + let id = object.id().ok_or_else(|| UpubError::field("id"))?.to_string(); -#[async_recursion::async_recursion] -async fn fetch_object_inner(ctx: &Context, id: &str, depth: usize) -> crate::Result { - if let Some(x) = model::object::Entity::find_by_id(id).one(ctx.db()).await? { - return Ok(x); // already in db, easy - } - - let object = Context::request( - Method::GET, id, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), - ).await?.json::().await?; - - if let Some(oid) = object.id() { - if oid != id { - if let Some(x) = model::object::Entity::find_by_id(oid).one(ctx.db()).await? { - return Ok(x); // already in db, but with id different that given url + if let Some(oid) = object.id() { + if oid != id { + if let Some(x) = model::object::Entity::find_by_ap_id(oid).one(self.db()).await? { + return Ok(x); // already in db, but with id different that given url + } } } - } - if let Some(attributed_to) = object.attributed_to().id() { - if let Err(e) = ctx.fetch_user(&attributed_to).await { - tracing::warn!("could not get actor of fetched object: {e}"); + if let Some(attributed_to) = object.attributed_to().id() { + if let Err(e) = self.fetch_user(&attributed_to).await { + tracing::warn!("could not get actor of fetched object: {e}"); + } } - } - let addressed = object.addressed(); + let addressed = object.addressed(); - if let Some(reply) = object.in_reply_to().id() { - if depth <= 16 { - fetch_object_inner(ctx, &reply, depth + 1).await?; - } else { - tracing::warn!("thread deeper than 16, giving up fetching more replies"); + if let Some(reply) = object.in_reply_to().id() { + if depth <= self.cfg().security.thread_crawl_depth { + self.fetch_object_r(&reply, depth + 1).await?; + } else { + tracing::warn!("thread deeper than {}, giving up fetching more replies", self.cfg().security.thread_crawl_depth); + } } + + let object_model = self.insert_object(object, None).await?; + + let expanded_addresses = self.expand_addressing(addressed).await?; + self.address_to(None, Some(object_model.internal), &expanded_addresses).await?; + + Ok(object_model) } - - let object_model = ctx.insert_object(object, None).await?; - - let expanded_addresses = ctx.expand_addressing(addressed).await?; - ctx.address_to(None, Some(&object_model.id), &expanded_addresses).await?; - - Ok(object_model) } #[axum::async_trait] @@ -329,9 +388,7 @@ pub trait Fetchable : Sync + Send { impl Fetchable for apb::Node { async fn fetch(&mut self, ctx: &crate::server::Context) -> crate::Result<&mut Self> { if let apb::Node::Link(uri) = self { - let from = format!("{}{}", ctx.protocol(), ctx.domain()); // TODO helper to avoid this? - let pkey = &ctx.app().private_key; - *self = Context::request(Method::GET, uri.href(), None, &from, pkey, ctx.domain()) + *self = Context::request(Method::GET, uri.href(), None, ctx.base(), ctx.pkey(), ctx.domain()) .await? .json::() .await? @@ -341,3 +398,56 @@ impl Fetchable for apb::Node { Ok(self) } } + +// #[async_recursion::async_recursion] +// async fn crawl_replies(ctx: &Context, id: &str, depth: usize) -> crate::Result<()> { +// tracing::info!("crawling replies of '{id}'"); +// let object = Context::request( +// Method::GET, id, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), +// ).await?.json::().await?; +// +// let object_model = model::object::Model::new(&object)?; +// match model::object::Entity::insert(object_model.into_active_model()) +// .exec(ctx.db()).await +// { +// Ok(_) => {}, +// Err(sea_orm::DbErr::RecordNotInserted) => {}, +// Err(sea_orm::DbErr::Exec(_)) => {}, // ughhh bad fix for sqlite +// Err(e) => return Err(e.into()), +// } +// +// if depth > 16 { +// tracing::warn!("stopping thread crawling: too deep!"); +// return Ok(()); +// } +// +// let mut page_url = match object.replies().get() { +// Some(serde_json::Value::String(x)) => { +// let replies = Context::request( +// Method::GET, x, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), +// ).await?.json::().await?; +// replies.first().id() +// }, +// Some(serde_json::Value::Object(x)) => { +// let obj = serde_json::Value::Object(x.clone()); // lol putting it back, TODO! +// obj.first().id() +// }, +// _ => return Ok(()), +// }; +// +// while let Some(ref url) = page_url { +// let replies = Context::request( +// Method::GET, url, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(), +// ).await?.json::().await?; +// +// for reply in replies.items() { +// // TODO right now it crawls one by one, could be made in parallel but would be quite more +// // abusive, so i'll keep it like this while i try it out +// crawl_replies(ctx, reply.href(), depth + 1).await?; +// } +// +// page_url = replies.next().id(); +// } +// +// Ok(()) +// } diff --git a/src/server/inbox.rs b/src/server/inbox.rs index 1a10650..12f74b8 100644 --- a/src/server/inbox.rs +++ b/src/server/inbox.rs @@ -1,10 +1,10 @@ use apb::{target::Addressed, Activity, Base, Object}; use reqwest::StatusCode; -use sea_orm::{sea_query::Expr, ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns, Set}; +use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; -use crate::{errors::{LoggableError, UpubError}, model::{self, FieldError}, server::normalizer::Normalizer}; +use crate::{errors::{LoggableError, UpubError}, model, server::{addresser::Addresser, builders::AnyQuery, normalizer::Normalizer}}; -use super::{fetcher::Fetcher, Context}; +use super::{fetcher::{Fetcher, PullResult}, side_effects::SideEffects, Context}; #[axum::async_trait] @@ -13,250 +13,233 @@ impl apb::server::Inbox for Context { type Activity = serde_json::Value; async fn create(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { - let activity_model = model::activity::Model::new(&activity)?; - let aid = activity_model.id.clone(); let Some(object_node) = activity.object().extract() else { // TODO we could process non-embedded activities or arrays but im lazy rn tracing::error!("refusing to process activity without embedded object: {}", serde_json::to_string_pretty(&activity).unwrap()); return Err(UpubError::unprocessable()); }; + if let Some(reply) = object_node.in_reply_to().id() { + if let Err(e) = self.fetch_object(&reply).await { + tracing::warn!("failed fetching replies for received object: {e}"); + } + } + let activity_model = self.insert_activity(activity, Some(server.clone())).await?; let object_model = self.insert_object(object_node, Some(server)).await?; - let expanded_addressing = self.expand_addressing(activity.addressed()).await?; - self.address_to(Some(&aid), Some(&object_model.id), &expanded_addressing).await?; - tracing::info!("{} posted {}", aid, object_model.id); + let expanded_addressing = self.expand_addressing(activity_model.addressed()).await?; + self.address_to(Some(activity_model.internal), Some(object_model.internal), &expanded_addressing).await?; + tracing::info!("{} posted {}", activity_model.actor, object_model.id); Ok(()) } - async fn like(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { - let aid = activity.id().ok_or(UpubError::bad_request())?; + async fn like(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { let uid = activity.actor().id().ok_or(UpubError::bad_request())?; + let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; let object_uri = activity.object().id().ok_or(UpubError::bad_request())?; let obj = self.fetch_object(&object_uri).await?; - let oid = obj.id; - let like = model::like::ActiveModel { - id: sea_orm::ActiveValue::NotSet, - actor: sea_orm::Set(uid.clone()), - likes: sea_orm::Set(oid.clone()), - date: sea_orm::Set(activity.published().unwrap_or(chrono::Utc::now())), - }; - match model::like::Entity::insert(like).exec(self.db()).await { - Err(sea_orm::DbErr::RecordNotInserted) => Err(UpubError::not_modified()), - Err(sea_orm::DbErr::Exec(_)) => Err(UpubError::not_modified()), // bad fix for sqlite - Err(e) => { - tracing::error!("unexpected error procesing like from {uid} to {oid}: {e}"); - Err(UpubError::internal_server_error()) - } - Ok(_) => { - let activity_model = model::activity::Model::new(&activity)?.into_active_model(); - model::activity::Entity::insert(activity_model) - .exec(self.db()) - .await?; - let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; - if expanded_addressing.is_empty() { // WHY MASTODON!!!!!!! - expanded_addressing.push( - model::object::Entity::find_by_id(&oid) - .select_only() - .select_column(model::object::Column::AttributedTo) - .into_tuple::() - .one(self.db()) - .await? - .ok_or_else(UpubError::not_found)? - ); - } - self.address_to(Some(aid), None, &expanded_addressing).await?; - model::object::Entity::update_many() - .col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).add(1)) - .filter(model::object::Column::Id.eq(oid.clone())) - .exec(self.db()) - .await?; - tracing::info!("{} liked {}", uid, oid); - Ok(()) - }, + if model::like::Entity::find_by_uid_oid(internal_uid, obj.internal) + .any(self.db()) + .await? + { + return Err(UpubError::not_modified()); } + + let activity_model = self.insert_activity(activity, Some(server)).await?; + self.process_like(internal_uid, obj.internal, activity_model.internal, activity_model.published).await?; + let mut expanded_addressing = self.expand_addressing(activity_model.addressed()).await?; + if expanded_addressing.is_empty() { // WHY MASTODON!!!!!!! + expanded_addressing.push( + model::object::Entity::find_by_id(obj.internal) + .select_only() + .select_column(model::object::Column::AttributedTo) + .into_tuple::() + .one(self.db()) + .await? + .ok_or_else(UpubError::not_found)? + ); + } + self.address_to(Some(activity_model.internal), None, &expanded_addressing).await?; + tracing::info!("{} liked {}", uid, obj.id); + Ok(()) } async fn follow(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { - let activity_model = model::activity::Model::new(&activity)?; - let aid = activity_model.id.clone(); - let target_user_uri = activity_model.object - .as_deref() - .ok_or_else(UpubError::bad_request)? - .to_string(); - let usr = self.fetch_user(&target_user_uri).await?; - let target_user_id = usr.id; - tracing::info!("{} wants to follow {}", activity_model.actor, target_user_id); - model::activity::Entity::insert(activity_model.into_active_model()) + let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string(); + let source_actor = activity.actor().id().ok_or_else(UpubError::bad_request)?; + let source_actor_internal = model::actor::Entity::ap_to_internal(&source_actor, self.db()).await?; + let target_actor = activity.object().id().ok_or_else(UpubError::bad_request)?; + let usr = self.fetch_user(&target_actor).await?; + let activity_model = model::activity::ActiveModel::new(&activity)?; + model::activity::Entity::insert(activity_model) + .exec(self.db()).await?; + let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?; + let relation_model = model::relation::ActiveModel { + internal: NotSet, + accept: Set(None), + activity: Set(internal_aid), + follower: Set(source_actor_internal), + following: Set(usr.internal), + }; + model::relation::Entity::insert(relation_model) .exec(self.db()).await?; let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; - if !expanded_addressing.contains(&target_user_id) { - expanded_addressing.push(target_user_id); + if !expanded_addressing.contains(&target_actor) { + expanded_addressing.push(target_actor); } - self.address_to(Some(&aid), None, &expanded_addressing).await?; + self.address_to(Some(internal_aid), None, &expanded_addressing).await?; + tracing::info!("{} wants to follow {}", source_actor, usr.id); Ok(()) } async fn accept(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { // TODO what about TentativeAccept - let activity_model = model::activity::Model::new(&activity)?; - - if let Some(mut r) = model::relay::Entity::find_by_id(&activity_model.actor) + let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string(); + let target_actor = activity.actor().id().ok_or_else(UpubError::bad_request)?; + let follow_request_id = activity.object().id().ok_or_else(UpubError::bad_request)?; + let follow_activity = model::activity::Entity::find_by_ap_id(&follow_request_id) .one(self.db()) .await? - { - r.accepted = true; - model::relay::Entity::update(r.into_active_model()).exec(self.db()).await?; - model::activity::Entity::insert(activity_model.clone().into_active_model()) - .exec(self.db()) - .await?; - tracing::info!("relay {} is now broadcasting to us", activity_model.actor); - return Ok(()); - } + .ok_or_else(UpubError::not_found)?; - let Some(follow_request_id) = &activity_model.object else { - return Err(UpubError::bad_request()); - }; - let Some(follow_activity) = model::activity::Entity::find_by_id(follow_request_id) - .one(self.db()).await? - else { - return Err(UpubError::not_found()); - }; - if follow_activity.object.unwrap_or("".into()) != activity_model.actor { + if follow_activity.object.unwrap_or("".into()) != target_actor { return Err(UpubError::forbidden()); } - tracing::info!("{} accepted follow request by {}", activity_model.actor, follow_activity.actor); + let activity_model = model::activity::ActiveModel::new(&activity)?; + model::activity::Entity::insert(activity_model) + .exec(self.db()) + .await?; + let accept_internal_id = model::activity::Entity::ap_to_internal(&aid, self.db()).await?; - model::activity::Entity::insert(activity_model.clone().into_active_model()) - .exec(self.db()) - .await?; - model::user::Entity::update_many() + model::actor::Entity::update_many() .col_expr( - model::user::Column::FollowingCount, - Expr::col(model::user::Column::FollowingCount).add(1) + model::actor::Column::FollowingCount, + Expr::col(model::actor::Column::FollowingCount).add(1) ) - .filter(model::user::Column::Id.eq(&follow_activity.actor)) + .filter(model::actor::Column::Id.eq(&follow_activity.actor)) .exec(self.db()) .await?; - model::relation::Entity::insert( - model::relation::ActiveModel { - follower: Set(follow_activity.actor.clone()), - following: Set(activity_model.actor), - ..Default::default() - } - ).exec(self.db()).await?; + model::actor::Entity::update_many() + .col_expr( + model::actor::Column::FollowersCount, + Expr::col(model::actor::Column::FollowersCount).add(1) + ) + .filter(model::actor::Column::Id.eq(&follow_activity.actor)) + .exec(self.db()) + .await?; + + model::relation::Entity::update_many() + .col_expr(model::relation::Column::Accept, Expr::value(Some(accept_internal_id))) + .filter(model::relation::Column::Activity.eq(follow_activity.internal)) + .exec(self.db()).await?; + + tracing::info!("{} accepted follow request by {}", target_actor, follow_activity.actor); let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; if !expanded_addressing.contains(&follow_activity.actor) { expanded_addressing.push(follow_activity.actor); } - self.address_to(Some(&activity_model.id), None, &expanded_addressing).await?; + self.address_to(Some(accept_internal_id), None, &expanded_addressing).await?; Ok(()) } async fn reject(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { // TODO what about TentativeReject? - let activity_model = model::activity::Model::new(&activity)?; - let Some(follow_request_id) = &activity_model.object else { - return Err(UpubError::bad_request()); - }; - let Some(follow_activity) = model::activity::Entity::find_by_id(follow_request_id) - .one(self.db()).await? - else { - return Err(UpubError::not_found()); - }; - if follow_activity.object.unwrap_or("".into()) != activity_model.actor { + let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string(); + let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?; + let follow_request_id = activity.object().id().ok_or_else(UpubError::bad_request)?; + let follow_activity = model::activity::Entity::find_by_ap_id(&follow_request_id) + .one(self.db()) + .await? + .ok_or_else(UpubError::not_found)?; + + if follow_activity.object.unwrap_or("".into()) != uid { return Err(UpubError::forbidden()); } - tracing::info!("{} rejected follow request by {}", activity_model.actor, follow_activity.actor); - - model::activity::Entity::insert(activity_model.clone().into_active_model()) + let activity_model = model::activity::ActiveModel::new(&activity)?; + model::activity::Entity::insert(activity_model) .exec(self.db()) .await?; + let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?; + + model::relation::Entity::delete_many() + .filter(model::relation::Column::Activity.eq(internal_aid)) + .exec(self.db()) + .await?; + + tracing::info!("{} rejected follow request by {}", uid, follow_activity.actor); let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; if !expanded_addressing.contains(&follow_activity.actor) { expanded_addressing.push(follow_activity.actor); } - self.address_to(Some(&activity_model.id), None, &expanded_addressing).await?; + + self.address_to(Some(internal_aid), None, &expanded_addressing).await?; Ok(()) } async fn delete(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { - // TODO verify the signature before just deleting lmao - let oid = activity.object().id().ok_or(UpubError::bad_request())?; - tracing::debug!("deleting '{oid}'"); // this is so spammy wtf! - // TODO maybe we should keep the tombstone? - model::user::Entity::delete_by_id(&oid).exec(self.db()).await.info_failed("failed deleting from users"); - model::activity::Entity::delete_by_id(&oid).exec(self.db()).await.info_failed("failed deleting from activities"); - model::object::Entity::delete_by_id(&oid).exec(self.db()).await.info_failed("failed deleting from objects"); + let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; + model::actor::Entity::delete_by_ap_id(&oid).exec(self.db()).await.info_failed("failed deleting from users"); + model::object::Entity::delete_by_ap_id(&oid).exec(self.db()).await.info_failed("failed deleting from objects"); + tracing::debug!("deleted '{oid}'"); Ok(()) } - async fn update(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { - let activity_model = model::activity::Model::new(&activity)?; - let aid = activity_model.id.clone(); + async fn update(&self, _server: String, activity: serde_json::Value) -> crate::Result<()> { + let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?; + let aid = activity.id().ok_or_else(UpubError::bad_request)?; let Some(object_node) = activity.object().extract() else { // TODO we could process non-embedded activities or arrays but im lazy rn tracing::error!("refusing to process activity without embedded object: {}", serde_json::to_string_pretty(&activity).unwrap()); return Err(UpubError::unprocessable()); }; - let Some(oid) = object_node.id().map(|x| x.to_string()) else { - return Err(UpubError::bad_request()); - }; - // make sure we're allowed to edit this object - if let Some(object_author) = object_node.attributed_to().id() { - if server != Context::server(&object_author) { - return Err(UpubError::forbidden()); - } - } else if server != Context::server(&oid) { - return Err(UpubError::forbidden()); - }; - match object_node.object_type() { - Some(apb::ObjectType::Actor(_)) => { - // TODO oof here is an example of the weakness of this model, we have to go all the way - // back up to serde_json::Value because impl Object != impl Actor - let actor_model = model::user::Model::new(&object_node)?; - let mut update_model = actor_model.into_active_model(); - update_model.updated = sea_orm::Set(chrono::Utc::now()); - update_model.reset(model::user::Column::Name); - update_model.reset(model::user::Column::Summary); - update_model.reset(model::user::Column::Image); - update_model.reset(model::user::Column::Icon); - model::user::Entity::update(update_model) - .exec(self.db()).await?; - }, - Some(apb::ObjectType::Note) => { - let object_model = model::object::Model::new(&object_node)?; - let mut update_model = object_model.into_active_model(); - update_model.updated = sea_orm::Set(Some(chrono::Utc::now())); - update_model.reset(model::object::Column::Name); - update_model.reset(model::object::Column::Summary); - update_model.reset(model::object::Column::Content); - update_model.reset(model::object::Column::Sensitive); - model::object::Entity::update(update_model) - .exec(self.db()).await?; - }, - Some(t) => tracing::warn!("no side effects implemented for update type {t:?}"), - None => tracing::warn!("empty type on embedded updated object"), - } + let oid = object_node.id().ok_or_else(UpubError::bad_request)?.to_string(); - tracing::info!("{} updated {}", aid, oid); - model::activity::Entity::insert(activity_model.into_active_model()) + let activity_model = model::activity::ActiveModel::new(&activity)?; + model::activity::Entity::insert(activity_model) .exec(self.db()) .await?; + let internal_aid = model::activity::Entity::ap_to_internal(aid, self.db()).await?; + + let internal_oid = match object_node.object_type().ok_or_else(UpubError::bad_request)? { + apb::ObjectType::Actor(_) => { + let internal_uid = model::actor::Entity::ap_to_internal(&oid, self.db()).await?; + let mut actor_model = model::actor::ActiveModel::new(&object_node)?; + actor_model.internal = Set(internal_uid); + actor_model.updated = Set(chrono::Utc::now()); + model::actor::Entity::update(actor_model) + .exec(self.db()) + .await?; + Some(internal_uid) + }, + apb::ObjectType::Note => { + let internal_oid = model::object::Entity::ap_to_internal(&oid, self.db()).await?; + let mut object_model = model::object::ActiveModel::new(&object_node)?; + object_model.internal = Set(internal_oid); + object_model.updated = Set(chrono::Utc::now()); + model::object::Entity::update(object_model) + .exec(self.db()) + .await?; + Some(internal_oid) + }, + t => { + tracing::warn!("no side effects implemented for update type {t:?}"); + None + }, + }; + + tracing::info!("{} updated {}", uid, oid); let expanded_addressing = self.expand_addressing(activity.addressed()).await?; - self.address_to(Some(&aid), Some(&oid), &expanded_addressing).await?; + self.address_to(Some(internal_aid), internal_oid, &expanded_addressing).await?; Ok(()) } async fn undo(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?; + let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; // TODO in theory we could work with just object_id but right now only accept embedded let undone_activity = activity.object().extract().ok_or_else(UpubError::bad_request)?; - let undone_aid = undone_activity.id().ok_or_else(UpubError::bad_request)?; - let undone_object_uri = undone_activity.object().id().ok_or_else(UpubError::bad_request)?; - let activity_type = undone_activity.activity_type().ok_or_else(UpubError::bad_request)?; let undone_activity_author = undone_activity.actor().id().ok_or_else(UpubError::bad_request)?; // can't undo activities from remote actors! @@ -264,83 +247,70 @@ impl apb::server::Inbox for Context { return Err(UpubError::forbidden()); }; - let obj = self.fetch_object(&undone_object_uri).await?; - let undone_object_id = obj.id; + let activity_model = self.insert_activity(activity.clone(), Some(server)).await?; - match activity_type { - apb::ActivityType::Like => { - model::like::Entity::delete_many() - .filter( - Condition::all() - .add(model::like::Column::Actor.eq(&uid)) - .add(model::like::Column::Likes.eq(&undone_object_id)) - ) - .exec(self.db()) - .await?; - model::object::Entity::update_many() - .filter(model::object::Column::Id.eq(&undone_object_id)) - .col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).sub(1)) - .exec(self.db()) - .await?; - }, - apb::ActivityType::Follow => { - model::relation::Entity::delete_many() - .filter( - Condition::all() - .add(model::relation::Column::Follower.eq(&uid)) - .add(model::relation::Column::Following.eq(&undone_object_id)) - ) - .exec(self.db()) - .await?; - }, - _ => { - tracing::error!("received 'Undo' for unimplemented activity: {}", serde_json::to_string_pretty(&activity).unwrap()); - return Err(StatusCode::NOT_IMPLEMENTED.into()); - }, - } + let targets = self.expand_addressing(activity.addressed()).await?; + self.process_undo(internal_uid, activity).await?; - model::activity::Entity::delete_by_id(undone_aid).exec(self.db()).await?; + self.address_to(Some(activity_model.internal), None, &targets).await?; Ok(()) - } - async fn announce(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { - let activity_model = model::activity::Model::new(&activity)?; - let Some(object_uri) = &activity_model.object else { - return Err(FieldError("object").into()); - }; - let obj = self.fetch_object(object_uri).await?; - let oid = obj.id; + async fn announce(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { + let uid = activity.actor().id().ok_or_else(|| UpubError::field("actor"))?; + let actor = self.fetch_user(&uid).await?; + let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; + let announced_id = activity.object().id().ok_or_else(|| UpubError::field("object"))?; + + match self.pull(&announced_id).await? { + PullResult::Actor(_) => Err(UpubError::unprocessable()), + PullResult::Object(object) => { + let object_model = self.resolve_object(object).await?; + let activity_model = self.insert_activity(activity.clone(), Some(server.clone())).await?; - // relays send us activities as Announce, but we don't really want to count those towards the - // total shares count of an object, so just fetch the object and be done with it - if self.is_relay(&activity_model.actor) { - tracing::info!("relay {} broadcasted {}", activity_model.actor, oid); - return Ok(()) + // relays send us objects as Announce, but we don't really want to count those towards the + // total shares count of an object, so just fetch the object and be done with it + if !matches!(actor.actor_type, apb::ActorType::Person) { + tracing::info!("relay {} broadcasted {}", activity_model.actor, announced_id); + return Ok(()) + } + + let share = model::announce::ActiveModel { + internal: NotSet, + actor: Set(internal_uid), + object: Set(object_model.internal), + published: Set(activity.published().unwrap_or(chrono::Utc::now())), + }; + + let expanded_addressing = self.expand_addressing(activity.addressed()).await?; + self.address_to(Some(activity_model.internal), None, &expanded_addressing).await?; + model::announce::Entity::insert(share) + .exec(self.db()).await?; + model::object::Entity::update_many() + .col_expr(model::object::Column::Announces, Expr::col(model::object::Column::Announces).add(1)) + .filter(model::object::Column::Internal.eq(object_model.internal)) + .exec(self.db()) + .await?; + + tracing::info!("{} shared {}", activity_model.actor, announced_id); + Ok(()) + }, + PullResult::Activity(activity) => { + // groups update all members of other things that happen inside, process those + let server = Context::server(activity.id().unwrap_or_default()); + match activity.activity_type().ok_or_else(UpubError::bad_request)? { + apb::ActivityType::Like | apb::ActivityType::EmojiReact => Ok(self.like(server, activity).await?), + apb::ActivityType::Create => Ok(self.create(server, activity).await?), + apb::ActivityType::Undo => Ok(self.undo(server, activity).await?), + apb::ActivityType::Delete => Ok(self.delete(server, activity).await?), + apb::ActivityType::Update => Ok(self.update(server, activity).await?), + x => { + tracing::warn!("ignoring unhandled announced activity of type {x:?}"); + Err(StatusCode::NOT_IMPLEMENTED.into()) + }, + } + }, } - - let share = model::share::ActiveModel { - id: sea_orm::ActiveValue::NotSet, - actor: sea_orm::Set(activity_model.actor.clone()), - shares: sea_orm::Set(oid.clone()), - date: sea_orm::Set(activity.published().unwrap_or(chrono::Utc::now())), - }; - - let expanded_addressing = self.expand_addressing(activity.addressed()).await?; - self.address_to(Some(&activity_model.id), None, &expanded_addressing).await?; - model::share::Entity::insert(share) - .exec(self.db()).await?; - model::activity::Entity::insert(activity_model.clone().into_active_model()) - .exec(self.db()) - .await?; - model::object::Entity::update_many() - .col_expr(model::object::Column::Shares, Expr::col(model::object::Column::Shares).add(1)) - .filter(model::object::Column::Id.eq(oid.clone())) - .exec(self.db()) - .await?; - - tracing::info!("{} shared {}", activity_model.actor, oid); - Ok(()) } } diff --git a/src/server/init.rs b/src/server/init.rs new file mode 100644 index 0000000..b154137 --- /dev/null +++ b/src/server/init.rs @@ -0,0 +1,71 @@ +use openssl::rsa::Rsa; +use sea_orm::{ActiveValue::{NotSet, Set}, DatabaseConnection, EntityTrait}; + +use crate::model; + +pub async fn application( + domain: String, + base_url: String, + db: &DatabaseConnection +) -> crate::Result<(model::actor::Model, model::instance::Model)> { + Ok(( + match model::actor::Entity::find_by_ap_id(&base_url).one(db).await? { + Some(model) => model, + None => { + tracing::info!("generating application keys"); + let rsa = Rsa::generate(2048)?; + let privk = std::str::from_utf8(&rsa.private_key_to_pem()?)?.to_string(); + let pubk = std::str::from_utf8(&rsa.public_key_to_pem()?)?.to_string(); + let system = model::actor::ActiveModel { + internal: NotSet, + id: Set(base_url.clone()), + domain: Set(domain.clone()), + preferred_username: Set(domain.clone()), + actor_type: Set(apb::ActorType::Application), + private_key: Set(Some(privk)), + public_key: Set(pubk), + following: Set(None), + following_count: Set(0), + followers: Set(None), + followers_count: Set(0), + statuses_count: Set(0), + summary: Set(Some("micro social network, federated".to_string())), + name: Set(Some("μpub".to_string())), + image: Set(None), + icon: Set(Some("https://cdn.alemi.dev/social/circle-square.png".to_string())), + inbox: Set(Some(format!("{base_url}/inbox"))), + shared_inbox: Set(Some(format!("{base_url}/inbox"))), + outbox: Set(Some(format!("{base_url}/outbox"))), + published: Set(chrono::Utc::now()), + updated: Set(chrono::Utc::now()), + }; + model::actor::Entity::insert(system).exec(db).await?; + // sqlite doesn't resurn last inserted id so we're better off just querying again, it's just one time + model::actor::Entity::find().one(db).await?.expect("could not find app actor just inserted") + } + }, + + match model::instance::Entity::find_by_domain(&domain).one(db).await? { + Some(model) => model, + None => { + tracing::info!("generating instance counters"); + let system = model::instance::ActiveModel { + internal: NotSet, + domain: Set(domain.clone()), + down_since: Set(None), + software: Set(Some("upub".to_string())), + version: Set(Some(crate::VERSION.to_string())), + name: Set(None), + icon: Set(None), + users: Set(Some(0)), + posts: Set(Some(0)), + published: Set(chrono::Utc::now()), + updated: Set(chrono::Utc::now()), + }; + model::instance::Entity::insert(system).exec(db).await?; + // sqlite doesn't resurn last inserted id so we're better off just querying again, it's just one time + model::instance::Entity::find().one(db).await?.expect("could not find app instance just inserted") + } + } + )) +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 392c281..1083e67 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,12 +1,15 @@ +pub mod addresser; pub mod admin; pub mod context; pub mod dispatcher; pub mod fetcher; pub mod inbox; +pub mod init; pub mod outbox; pub mod auth; pub mod builders; pub mod httpsign; pub mod normalizer; +pub mod side_effects; pub use context::Context; diff --git a/src/server/normalizer.rs b/src/server/normalizer.rs index f4fe2db..7b56662 100644 --- a/src/server/normalizer.rs +++ b/src/server/normalizer.rs @@ -1,34 +1,33 @@ use apb::{Node, Base, Object, Document}; -use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set}; +use sea_orm::{sea_query::Expr, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; use crate::{errors::UpubError, model, server::Context}; -use super::fetcher::Fetcher; - #[axum::async_trait] pub trait Normalizer { async fn insert_object(&self, obj: impl apb::Object, server: Option) -> crate::Result; + async fn insert_activity(&self, act: impl apb::Activity, server: Option) -> crate::Result; } #[axum::async_trait] impl Normalizer for super::Context { async fn insert_object(&self, object_node: impl apb::Object, server: Option) -> crate::Result { - let mut object_model = model::object::Model::new(&object_node)?; - let oid = object_model.id.clone(); - let uid = object_model.attributed_to.clone(); + let oid = object_node.id().ok_or_else(UpubError::bad_request)?.to_string(); + let uid = object_node.attributed_to().id(); + let mut object_model = model::object::ActiveModel::new(&object_node)?; if let Some(server) = server { // make sure we're allowed to create this object - if let Some(object_author) = &object_model.attributed_to { + if let Set(Some(object_author)) = &object_model.attributed_to { if server != Context::server(object_author) { return Err(UpubError::forbidden()); } - } else if server != Context::server(&object_model.id) { + } else if server != Context::server(&oid) { return Err(UpubError::forbidden()); }; } // make sure content only contains a safe subset of html - if let Some(content) = object_model.content { - object_model.content = Some(mdhtml::safe_html(&content)); + if let Set(Some(content)) = object_model.content { + object_model.content = Set(Some(mdhtml::safe_html(&content))); } // fix context for remote posts @@ -37,33 +36,32 @@ impl Normalizer for super::Context { // > btw! also if any link is broken or we get rate limited, the whole insertion fails which is // > kind of dumb. there should be a job system so this can be done in waves. or maybe there's // > some whole other way to do this?? im thinking but misskey aaaa!! TODO - if let Some(ref reply) = object_model.in_reply_to { - if let Some(o) = model::object::Entity::find_by_id(reply).one(self.db()).await? { - object_model.context = o.context; + if let Set(Some(ref reply)) = object_model.in_reply_to { + if let Some(o) = model::object::Entity::find_by_ap_id(reply).one(self.db()).await? { + object_model.context = Set(o.context); } else { - object_model.context = None; // TODO to be filled by some other task + object_model.context = Set(None); // TODO to be filled by some other task } } else { - object_model.context = Some(object_model.id.clone()); + object_model.context = Set(Some(oid.clone())); } model::object::Entity::insert(object_model.clone().into_active_model()).exec(self.db()).await?; + let object = model::object::Entity::find_by_ap_id(&oid).one(self.db()).await?.ok_or_else(UpubError::internal_server_error)?; // update replies counter - if let Some(ref in_reply_to) = object_model.in_reply_to { - if self.fetch_object(in_reply_to).await.is_ok() { - model::object::Entity::update_many() - .filter(model::object::Column::Id.eq(in_reply_to)) - .col_expr(model::object::Column::Comments, Expr::col(model::object::Column::Comments).add(1)) - .exec(self.db()) - .await?; - } + if let Set(Some(ref in_reply_to)) = object_model.in_reply_to { + model::object::Entity::update_many() + .filter(model::object::Column::Id.eq(in_reply_to)) + .col_expr(model::object::Column::Replies, Expr::col(model::object::Column::Replies).add(1)) + .exec(self.db()) + .await?; } // update statuses counter if let Some(object_author) = uid { - model::user::Entity::update_many() - .col_expr(model::user::Column::StatusesCount, Expr::col(model::user::Column::StatusesCount).add(1)) - .filter(model::user::Column::Id.eq(&object_author)) + model::actor::Entity::update_many() + .col_expr(model::actor::Column::StatusesCount, Expr::col(model::actor::Column::StatusesCount).add(1)) + .filter(model::actor::Column::Id.eq(&object_author)) .exec(self.db()) .await?; } @@ -76,22 +74,22 @@ impl Normalizer for super::Context { continue }, Node::Link(l) => model::attachment::ActiveModel { - id: sea_orm::ActiveValue::NotSet, + internal: sea_orm::ActiveValue::NotSet, url: Set(l.href().to_string()), - object: Set(oid.clone()), + object: Set(object.internal), document_type: Set(apb::DocumentType::Page), name: Set(l.link_name().map(|x| x.to_string())), media_type: Set(l.link_media_type().unwrap_or("link").to_string()), - created: Set(chrono::Utc::now()), + published: Set(chrono::Utc::now()), }, Node::Object(o) => model::attachment::ActiveModel { - id: sea_orm::ActiveValue::NotSet, + internal: sea_orm::ActiveValue::NotSet, url: Set(o.url().id().unwrap_or_else(|| o.id().map(|x| x.to_string()).unwrap_or_default())), - object: Set(oid.clone()), + object: Set(object.internal), document_type: Set(o.as_document().map_or(apb::DocumentType::Document, |x| x.document_type().unwrap_or(apb::DocumentType::Page))), name: Set(o.name().map(|x| x.to_string())), media_type: Set(o.media_type().unwrap_or("link").to_string()), - created: Set(o.published().unwrap_or_else(chrono::Utc::now)), + published: Set(o.published().unwrap_or_else(chrono::Utc::now)), }, }; model::attachment::Entity::insert(attachment_model) @@ -113,19 +111,50 @@ impl Normalizer for super::Context { }; let attachment_model = model::attachment::ActiveModel { - id: sea_orm::ActiveValue::NotSet, + internal: sea_orm::ActiveValue::NotSet, url: Set(img.url().id().unwrap_or_else(|| img.id().map(|x| x.to_string()).unwrap_or_default())), - object: Set(oid.clone()), + object: Set(object.internal), document_type: Set(img.as_document().map_or(apb::DocumentType::Document, |x| x.document_type().unwrap_or(apb::DocumentType::Page))), name: Set(img.name().map(|x| x.to_string())), media_type: Set(img.media_type().unwrap_or(media_type.as_deref().unwrap_or("link")).to_string()), - created: Set(img.published().unwrap_or_else(chrono::Utc::now)), + published: Set(img.published().unwrap_or_else(chrono::Utc::now)), }; model::attachment::Entity::insert(attachment_model) .exec(self.db()) .await?; } - Ok(object_model) + Ok(object) + } + + async fn insert_activity(&self, activity: impl apb::Activity, server: Option) -> crate::Result { + let mut activity_model = model::activity::Model { + internal: 0, + id: activity.id().ok_or_else(|| UpubError::field("id"))?.to_string(), + activity_type: activity.activity_type().ok_or_else(|| UpubError::field("type"))?, + actor: activity.actor().id().ok_or_else(|| UpubError::field("actor"))?, + object: activity.object().id(), + target: activity.target().id(), + published: activity.published().unwrap_or(chrono::Utc::now()), + to: activity.to().into(), + bto: activity.bto().into(), + cc: activity.cc().into(), + bcc: activity.bcc().into(), + }; + if let Some(server) = server { + if Context::server(&activity_model.actor) != server + || Context::server(&activity_model.id) != server { + return Err(UpubError::forbidden()); + } + } + let mut active_model = activity_model.clone().into_active_model(); + active_model.internal = NotSet; + model::activity::Entity::insert(active_model) + .exec(self.db()) + .await?; + + let internal = model::activity::Entity::ap_to_internal(&activity_model.id, self.db()).await?; + activity_model.internal = internal; + Ok(activity_model) } } diff --git a/src/server/outbox.rs b/src/server/outbox.rs index 6131b59..d3b55cd 100644 --- a/src/server/outbox.rs +++ b/src/server/outbox.rs @@ -1,10 +1,10 @@ -use apb::{target::Addressed, Activity, ActivityMut, ActorMut, BaseMut, Node, Object, ObjectMut, PublicKeyMut}; +use apb::{target::Addressed, Activity, ActivityMut, Base, BaseMut, Node, Object, ObjectMut}; use reqwest::StatusCode; -use sea_orm::{sea_query::Expr, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns, Set}; +use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet, Unchanged}, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns}; use crate::{errors::UpubError, model, routes::activitypub::jsonld::LD}; -use super::{fetcher::Fetcher, normalizer::Normalizer, Context}; +use super::{addresser::Addresser, builders::AnyQuery, fetcher::Fetcher, normalizer::Normalizer, side_effects::SideEffects, Context}; #[axum::async_trait] @@ -14,61 +14,16 @@ impl apb::server::Outbox for Context { type Activity = serde_json::Value; async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result { - // TODO regex hell, here i come... - let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern"); - let raw_oid = uuid::Uuid::new_v4().to_string(); - let oid = self.oid(&raw_oid); - let aid = self.aid(&uuid::Uuid::new_v4().to_string()); - let activity_targets = object.addressed(); - - let mut content = object.content().map(|x| x.to_string()); - if let Some(c) = content { - let mut tmp = mdhtml::safe_markdown(&c); - for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) { - if let Ok(Some(uid)) = model::user::Entity::find() - .filter(model::user::Column::PreferredUsername.eq(user)) - .filter(model::user::Column::Domain.eq(domain)) - .select_only() - .select_column(model::user::Column::Id) - .into_tuple::() - .one(self.db()) - .await - { - tmp = tmp.replacen(full, &format!("@{user}"), 1); - } - } - content = Some(tmp); - } - - let object_model = self.insert_object( - object - .set_id(Some(&oid)) - .set_attributed_to(Node::link(uid.clone())) - .set_published(Some(chrono::Utc::now())) - .set_content(content.as_deref()) - .set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))), - Some(self.domain().to_string()), - ).await?; - - let activity_model = model::activity::Model { - id: aid.clone(), - activity_type: apb::ActivityType::Create, - actor: uid.clone(), - object: Some(oid.clone()), - target: None, - cc: object_model.cc.clone(), - bcc: object_model.bcc.clone(), - to: object_model.to.clone(), - bto: object_model.bto.clone(), - published: object_model.published, - }; - - model::activity::Entity::insert(activity_model.into_active_model()) - .exec(self.db()).await?; - - self.dispatch(&uid, activity_targets, &aid, Some(&oid)).await?; - - Ok(aid) + self.create( + uid, + serde_json::Value::new_object() + .set_activity_type(Some(apb::ActivityType::Create)) + .set_to(object.to()) + .set_bto(object.bto()) + .set_cc(object.cc()) + .set_bcc(object.bcc()) + .set_object(Node::object(object)) + ).await } async fn create(&self, uid: String, activity: serde_json::Value) -> crate::Result { @@ -81,28 +36,49 @@ impl apb::server::Outbox for Context { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); + if let Some(reply) = object.in_reply_to().id() { + self.fetch_object(&reply).await?; + } + + // TODO regex hell here i come... + let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern"); + let mut content = object.content().map(|x| x.to_string()); + if let Some(c) = content { + let mut tmp = mdhtml::safe_markdown(&c); + for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) { + if let Ok(Some(uid)) = model::actor::Entity::find() + .filter(model::actor::Column::PreferredUsername.eq(user)) + .filter(model::actor::Column::Domain.eq(domain)) + .select_only() + .select_column(model::actor::Column::Id) + .into_tuple::() + .one(self.db()) + .await + { + tmp = tmp.replacen(full, &format!("@{user}"), 1); + } + } + content = Some(tmp); + } + self.insert_object( object .set_id(Some(&oid)) .set_attributed_to(Node::link(uid.clone())) .set_published(Some(chrono::Utc::now())) - .set_to(activity.to()) - .set_bto(activity.bto()) - .set_cc(activity.cc()) - .set_bcc(activity.bcc()), + .set_content(content.as_deref()) + .set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))), Some(self.domain().to_string()), ).await?; - let activity_model = model::activity::Model::new( - &activity + self.insert_activity( + activity .set_id(Some(&aid)) .set_actor(Node::link(uid.clone())) - .set_published(Some(chrono::Utc::now())) .set_object(Node::link(oid.clone())) - )?; - - model::activity::Entity::insert(activity_model.into_active_model()) - .exec(self.db()).await?; + .set_published(Some(chrono::Utc::now())), + Some(self.domain().to_string()), + ).await?; self.dispatch(&uid, activity_targets, &aid, Some(&oid)).await?; Ok(aid) @@ -112,28 +88,26 @@ impl apb::server::Outbox for Context { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; - self.fetch_object(&oid).await?; - let activity_model = model::activity::Model::new( - &activity - .set_id(Some(&aid)) - .set_published(Some(chrono::Utc::now())) - .set_actor(Node::link(uid.clone())) - )?; + let obj_model = self.fetch_object(&oid).await?; - let like_model = model::like::ActiveModel { - actor: Set(uid.clone()), - likes: Set(oid.clone()), - date: Set(chrono::Utc::now()), - ..Default::default() - }; - model::like::Entity::insert(like_model).exec(self.db()).await?; - model::activity::Entity::insert(activity_model.into_active_model()) - .exec(self.db()).await?; - model::object::Entity::update_many() - .col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).add(1)) - .filter(model::object::Column::Id.eq(oid)) - .exec(self.db()) - .await?; + let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; + + if model::like::Entity::find_by_uid_oid(internal_uid, obj_model.internal) + .any(self.db()) + .await? + { + return Err(UpubError::not_modified()); + } + + let activity_model = self.insert_activity( + activity + .set_id(Some(&aid)) + .set_actor(Node::link(uid.clone())) + .set_published(Some(chrono::Utc::now())), + Some(self.domain().to_string()), + ).await?; + + self.process_like(internal_uid, obj_model.internal, activity_model.internal, chrono::Utc::now()).await?; self.dispatch(&uid, activity_targets, &aid, None).await?; @@ -143,17 +117,32 @@ impl apb::server::Outbox for Context { async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); - if activity.object().id().is_none() { - return Err(UpubError::bad_request()); - } + let target = activity.object().id().ok_or_else(UpubError::bad_request)?; - let activity_model = model::activity::Model::new( + let activity_model = model::activity::ActiveModel::new( &activity .set_id(Some(&aid)) .set_actor(Node::link(uid.clone())) .set_published(Some(chrono::Utc::now())) )?; - model::activity::Entity::insert(activity_model.into_active_model()) + + let follower_internal = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; + let following_internal = model::actor::Entity::ap_to_internal(&target, self.db()).await?; + + model::activity::Entity::insert(activity_model) + .exec(self.db()).await?; + + let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?; + + let relation_model = model::relation::ActiveModel { + internal: NotSet, + follower: Set(follower_internal), + following: Set(following_internal), + activity: Set(internal_aid), + accept: Set(None), + }; + + model::relation::Entity::insert(relation_model) .exec(self.db()).await?; self.dispatch(&uid, activity_targets, &aid, None).await?; @@ -164,39 +153,19 @@ impl apb::server::Outbox for Context { async fn accept(&self, uid: String, activity: serde_json::Value) -> crate::Result { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); - if activity.object().id().is_none() { - return Err(UpubError::bad_request()); - } - let Some(accepted_id) = activity.object().id() else { - return Err(UpubError::bad_request()); - }; - let Some(accepted_activity) = model::activity::Entity::find_by_id(accepted_id) + let accepted_id = activity.object().id().ok_or_else(UpubError::bad_request)?; + let accepted_activity = model::activity::Entity::find_by_ap_id(&accepted_id) .one(self.db()).await? - else { - return Err(UpubError::not_found()); - }; + .ok_or_else(UpubError::not_found)?; - match accepted_activity.activity_type { - apb::ActivityType::Follow => { - model::user::Entity::update_many() - .col_expr( - model::user::Column::FollowersCount, - Expr::col(model::user::Column::FollowersCount).add(1) - ) - .filter(model::user::Column::Id.eq(&uid)) - .exec(self.db()) - .await?; - model::relation::Entity::insert( - model::relation::ActiveModel { - follower: Set(accepted_activity.actor), following: Set(uid.clone()), - ..Default::default() - } - ).exec(self.db()).await?; - }, - t => tracing::warn!("no side effects implemented for accepting {t:?}"), + if accepted_activity.activity_type != apb::ActivityType::Follow { + return Err(UpubError::bad_request()); + } + if uid != accepted_activity.object.ok_or_else(UpubError::bad_request)? { + return Err(UpubError::forbidden()); } - let activity_model = model::activity::Model::new( + let activity_model = model::activity::ActiveModel::new( &activity .set_id(Some(&aid)) .set_actor(Node::link(uid.clone())) @@ -205,54 +174,92 @@ impl apb::server::Outbox for Context { model::activity::Entity::insert(activity_model.into_active_model()) .exec(self.db()).await?; + let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?; + + match accepted_activity.activity_type { + apb::ActivityType::Follow => { + model::actor::Entity::update_many() + .col_expr( + model::actor::Column::FollowersCount, + Expr::col(model::actor::Column::FollowersCount).add(1) + ) + .filter(model::actor::Column::Id.eq(&uid)) + .exec(self.db()) + .await?; + model::relation::Entity::update_many() + .filter(model::relation::Column::Activity.eq(accepted_activity.internal)) + .col_expr(model::relation::Column::Accept, Expr::value(Some(internal_aid))) + .exec(self.db()).await?; + }, + t => tracing::error!("no side effects implemented for accepting {t:?}"), + } + self.dispatch(&uid, activity_targets, &aid, None).await?; Ok(aid) } - async fn reject(&self, _uid: String, _activity: serde_json::Value) -> crate::Result { - todo!() + async fn reject(&self, uid: String, activity: serde_json::Value) -> crate::Result { + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); + let activity_targets = activity.addressed(); + let rejected_id = activity.object().id().ok_or_else(UpubError::bad_request)?; + let rejected_activity = model::activity::Entity::find_by_ap_id(&rejected_id) + .one(self.db()).await? + .ok_or_else(UpubError::not_found)?; + + if rejected_activity.activity_type != apb::ActivityType::Follow { + return Err(UpubError::bad_request()); + } + if uid != rejected_activity.object.ok_or_else(UpubError::bad_request)? { + return Err(UpubError::forbidden()); + } + + let activity_model = model::activity::ActiveModel::new( + &activity + .set_id(Some(&aid)) + .set_actor(Node::link(uid.clone())) + .set_published(Some(chrono::Utc::now())) + )?; + model::activity::Entity::insert(activity_model) + .exec(self.db()).await?; + + let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?; + + model::relation::Entity::delete_many() + .filter(model::relation::Column::Activity.eq(internal_aid)) + .exec(self.db()) + .await?; + + self.dispatch(&uid, activity_targets, &aid, None).await?; + + Ok(aid) } async fn undo(&self, uid: String, activity: serde_json::Value) -> crate::Result { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); - let activity_targets = activity.addressed(); + let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; let old_aid = activity.object().id().ok_or_else(UpubError::bad_request)?; - let old_activity = model::activity::Entity::find_by_id(old_aid) + let old_activity = model::activity::Entity::find_by_ap_id(&old_aid) .one(self.db()) .await? .ok_or_else(UpubError::not_found)?; if old_activity.actor != uid { return Err(UpubError::forbidden()); } - match old_activity.activity_type { - apb::ActivityType::Like => { - model::like::Entity::delete_many() - .filter(model::like::Column::Actor.eq(old_activity.actor)) - .filter(model::like::Column::Likes.eq(old_activity.object.unwrap_or("".into()))) - .exec(self.db()) - .await?; - }, - apb::ActivityType::Follow => { - model::relation::Entity::delete_many() - .filter(model::relation::Column::Follower.eq(old_activity.actor)) - .filter(model::relation::Column::Following.eq(old_activity.object.unwrap_or("".into()))) - .exec(self.db()) - .await?; - }, - t => tracing::warn!("extra side effects for activity {t:?} not implemented"), - } - let activity_model = model::activity::Model::new( - &activity + + let activity_model = self.insert_activity( + activity.clone() .set_id(Some(&aid)) .set_actor(Node::link(uid.clone())) - .set_published(Some(chrono::Utc::now())) - )?; - model::activity::Entity::insert(activity_model.into_active_model()) - .exec(self.db()) - .await?; + .set_published(Some(chrono::Utc::now())), + Some(self.domain().to_string()) + ).await?; - self.dispatch(&uid, activity_targets, &aid, None).await?; + let targets = self.expand_addressing(activity.addressed()).await?; + self.process_undo(internal_uid, activity).await?; + + self.address_to(Some(activity_model.internal), None, &targets).await?; + self.deliver_to(&activity_model.id, &uid, &targets).await?; Ok(aid) } @@ -261,34 +268,29 @@ impl apb::server::Outbox for Context { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; - let object = model::object::Entity::find_by_id(&oid) + let object = model::object::Entity::find_by_ap_id(&oid) .one(self.db()) .await? .ok_or_else(UpubError::not_found)?; - let Some(author_id) = object.attributed_to else { - // can't change local objects attributed to nobody - return Err(UpubError::forbidden()) - }; - - if author_id != uid { - // can't change objects of others + if uid != object.attributed_to.ok_or_else(UpubError::forbidden)? { + // can't change objects of others, and objects from noone count as others return Err(UpubError::forbidden()); } let addressed = activity.addressed(); - let activity_model = model::activity::Model::new( + let activity_model = model::activity::ActiveModel::new( &activity .set_id(Some(&aid)) .set_actor(Node::link(uid.clone())) .set_published(Some(chrono::Utc::now())) )?; - model::object::Entity::delete_by_id(&oid) + model::activity::Entity::insert(activity_model) .exec(self.db()) .await?; - model::activity::Entity::insert(activity_model.into_active_model()) + model::object::Entity::delete_by_ap_id(&oid) .exec(self.db()) .await?; @@ -300,18 +302,22 @@ impl apb::server::Outbox for Context { async fn update(&self, uid: String, activity: serde_json::Value) -> crate::Result { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let object_node = activity.object().extract().ok_or_else(UpubError::bad_request)?; + let addressed = activity.addressed(); + let target = object_node.id().ok_or_else(UpubError::bad_request)?.to_string(); + + let activity_model = model::activity::ActiveModel::new( + &activity + .set_id(Some(&aid)) + .set_actor(Node::link(uid.clone())) + .set_published(Some(chrono::Utc::now())) + )?; + + model::activity::Entity::insert(activity_model) + .exec(self.db()).await?; match object_node.object_type() { Some(apb::ObjectType::Actor(_)) => { - let mut actor_model = model::user::Model::new( - &object_node - // TODO must set these, but we will ignore them - .set_actor_type(Some(apb::ActorType::Person)) - .set_public_key(apb::Node::object( - serde_json::Value::new_object().set_public_key_pem("") - )) - )?; - let old_actor_model = model::user::Entity::find_by_id(&actor_model.id) + let old_actor_model = model::actor::Entity::find_by_ap_id(&target) .one(self.db()) .await? .ok_or_else(UpubError::not_found)?; @@ -321,66 +327,61 @@ impl apb::server::Outbox for Context { return Err(UpubError::forbidden()); } - if actor_model.name.is_none() { actor_model.name = old_actor_model.name } - if actor_model.summary.is_none() { actor_model.summary = old_actor_model.summary } - if actor_model.image.is_none() { actor_model.image = old_actor_model.image } - if actor_model.icon.is_none() { actor_model.icon = old_actor_model.icon } + let mut new_actor_model = model::actor::ActiveModel { + internal: Unchanged(old_actor_model.internal), + ..Default::default() + }; - let mut update_model = actor_model.into_active_model(); - update_model.updated = sea_orm::Set(chrono::Utc::now()); - update_model.reset(model::user::Column::Name); - update_model.reset(model::user::Column::Summary); - update_model.reset(model::user::Column::Image); - update_model.reset(model::user::Column::Icon); + if let Some(name) = object_node.name() { + new_actor_model.name = Set(Some(name.to_string())); + } + if let Some(summary) = object_node.summary() { + new_actor_model.summary = Set(Some(summary.to_string())); + } + if let Some(image) = object_node.image().id() { + new_actor_model.image = Set(Some(image)); + } + if let Some(icon) = object_node.icon().id() { + new_actor_model.icon = Set(Some(icon)); + } + new_actor_model.updated = Set(chrono::Utc::now()); - model::user::Entity::update(update_model) + model::actor::Entity::update(new_actor_model) .exec(self.db()).await?; }, Some(apb::ObjectType::Note) => { - let mut object_model = model::object::Model::new( - &object_node.set_published(Some(chrono::Utc::now())) - )?; - - let old_object_model = model::object::Entity::find_by_id(&object_model.id) + let old_object_model = model::object::Entity::find_by_ap_id(&target) .one(self.db()) .await? .ok_or_else(UpubError::not_found)?; - // can't change local objects attributed to nobody - let author_id = old_object_model.attributed_to.ok_or_else(UpubError::forbidden)?; - if author_id != uid { + if uid != old_object_model.attributed_to.ok_or_else(UpubError::forbidden)? { // can't change objects of others return Err(UpubError::forbidden()); } - if object_model.name.is_none() { object_model.name = old_object_model.name } - if object_model.summary.is_none() { object_model.summary = old_object_model.summary } - if object_model.content.is_none() { object_model.content = old_object_model.content } + let mut new_object_model = model::object::ActiveModel { + internal: Unchanged(old_object_model.internal), + ..Default::default() + }; - let mut update_model = object_model.into_active_model(); - update_model.updated = sea_orm::Set(Some(chrono::Utc::now())); - update_model.reset(model::object::Column::Name); - update_model.reset(model::object::Column::Summary); - update_model.reset(model::object::Column::Content); - update_model.reset(model::object::Column::Sensitive); + if let Some(name) = object_node.name() { + new_object_model.name = Set(Some(name.to_string())); + } + if let Some(summary) = object_node.summary() { + new_object_model.summary = Set(Some(summary.to_string())); + } + if let Some(content) = object_node.content() { + new_object_model.content = Set(Some(content.to_string())); + } + new_object_model.updated = Set(chrono::Utc::now()); - model::object::Entity::update(update_model) + model::object::Entity::update(new_object_model) .exec(self.db()).await?; }, _ => return Err(UpubError::Status(StatusCode::NOT_IMPLEMENTED)), } - let addressed = activity.addressed(); - let activity_model = model::activity::Model::new( - &activity - .set_id(Some(&aid)) - .set_actor(Node::link(uid.clone())) - .set_published(Some(chrono::Utc::now())) - )?; - - model::activity::Entity::insert(activity_model.into_active_model()) - .exec(self.db()).await?; - self.dispatch(&uid, addressed, &aid, None).await?; Ok(aid) @@ -390,26 +391,28 @@ impl apb::server::Outbox for Context { let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; - self.fetch_object(&oid).await?; - let activity_model = model::activity::Model::new( + let obj = self.fetch_object(&oid).await?; + let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?; + + let activity_model = model::activity::ActiveModel::new( &activity .set_id(Some(&aid)) - .set_published(Some(chrono::Utc::now())) .set_actor(Node::link(uid.clone())) + .set_published(Some(chrono::Utc::now())) )?; - let share_model = model::share::ActiveModel { - actor: Set(uid.clone()), - shares: Set(oid.clone()), - date: Set(chrono::Utc::now()), - ..Default::default() + let share_model = model::announce::ActiveModel { + internal: NotSet, + actor: Set(internal_uid), + object: Set(obj.internal), + published: Set(chrono::Utc::now()), }; - model::share::Entity::insert(share_model).exec(self.db()).await?; - model::activity::Entity::insert(activity_model.into_active_model()) + model::activity::Entity::insert(activity_model) .exec(self.db()).await?; + model::announce::Entity::insert(share_model).exec(self.db()).await?; model::object::Entity::update_many() - .col_expr(model::object::Column::Shares, Expr::col(model::object::Column::Shares).add(1)) - .filter(model::object::Column::Id.eq(oid)) + .col_expr(model::object::Column::Announces, Expr::col(model::object::Column::Announces).add(1)) + .filter(model::object::Column::Internal.eq(obj.internal)) .exec(self.db()) .await?; diff --git a/src/server/side_effects.rs b/src/server/side_effects.rs new file mode 100644 index 0000000..40092f8 --- /dev/null +++ b/src/server/side_effects.rs @@ -0,0 +1,79 @@ +use reqwest::StatusCode; +use sea_orm::{sea_query::Expr, ActiveValue::{NotSet, Set}, ColumnTrait, Condition, EntityTrait, QueryFilter}; + +use crate::{errors::UpubError, model}; + +#[axum::async_trait] +pub trait SideEffects { + async fn process_like(&self, who: i64, what: i64, with: i64, when: chrono::DateTime) -> crate::Result<()>; + async fn process_undo(&self, who: i64, activity: impl apb::Activity) -> crate::Result<()>; +} + +#[axum::async_trait] +impl SideEffects for super::Context { + async fn process_like(&self, who: i64, what: i64, with: i64, when: chrono::DateTime) -> crate::Result<()> { + let like = model::like::ActiveModel { + internal: NotSet, + actor: Set(who), + object: Set(what), + activity: Set(with), + published: Set(when), + }; + model::like::Entity::insert(like).exec(self.db()).await?; + model::object::Entity::update_many() + .col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).add(1)) + .filter(model::object::Column::Internal.eq(what)) + .exec(self.db()) + .await?; + + Ok(()) + } + + async fn process_undo(&self, who: i64, activity: impl apb::Activity) -> crate::Result<()> { + let undone_object_id = activity.object().id().ok_or_else(UpubError::bad_request)?; + match activity.activity_type() { + Some(apb::ActivityType::Like) => { + let internal_oid = model::object::Entity::ap_to_internal(&undone_object_id, self.db()).await?; + model::like::Entity::delete_many() + .filter( + Condition::all() + .add(model::like::Column::Actor.eq(who)) + .add(model::like::Column::Object.eq(internal_oid)) + ) + .exec(self.db()) + .await?; + model::object::Entity::update_many() + .filter(model::object::Column::Internal.eq(internal_oid)) + .col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).sub(1)) + .exec(self.db()) + .await?; + }, + Some(apb::ActivityType::Follow) => { + let undone_aid = activity.object().id().ok_or_else(UpubError::bad_request)?; + let internal_aid = model::activity::Entity::ap_to_internal(&undone_aid, self.db()).await?; + model::relation::Entity::delete_many() + .filter(model::relation::Column::Activity.eq(internal_aid)) + .exec(self.db()) + .await?; + model::actor::Entity::update_many() + .filter(model::actor::Column::Internal.eq(who)) + .col_expr(model::actor::Column::FollowingCount, Expr::col(model::actor::Column::FollowingCount).sub(1)) + .exec(self.db()) + .await?; + model::actor::Entity::update_many() + .filter(model::actor::Column::Id.eq(&undone_object_id)) + .col_expr(model::actor::Column::FollowersCount, Expr::col(model::actor::Column::FollowersCount).sub(1)) + .exec(self.db()) + .await?; + }, + t => { + tracing::error!("received 'Undo' for unimplemented activity type: {t:?}"); + return Err(StatusCode::NOT_IMPLEMENTED.into()); + }, + } + + + Ok(()) + } + +} diff --git a/utils/mdhtml/src/lib.rs b/utils/mdhtml/src/lib.rs index 485d238..a7a0368 100644 --- a/utils/mdhtml/src/lib.rs +++ b/utils/mdhtml/src/lib.rs @@ -23,37 +23,44 @@ impl TokenSink for Sink { ) { return TokenSinkResult::Continue } // skip this tag self.0.push('<'); + if !tag.self_closing && matches!(tag.kind, TagKind::EndTag) { self.0.push('/'); } self.0.push_str(tag.name.as_ref()); - match tag.name.as_ref() { - "img" => for attr in tag.attrs { - match attr.name.local.as_ref() { - "src" => self.0.push_str(&format!(" src=\"{}\"", attr.value.as_ref())), - "title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())), - "alt" => self.0.push_str(&format!(" alt=\"{}\"", attr.value.as_ref())), - _ => {}, - } - }, - "a" => { - for attr in tag.attrs { + if !matches!(tag.kind, TagKind::EndTag) { + match tag.name.as_ref() { + "img" => for attr in tag.attrs { match attr.name.local.as_ref() { - "href" => self.0.push_str(&format!(" href=\"{}\"", attr.value.as_ref())), + "src" => self.0.push_str(&format!(" src=\"{}\"", attr.value.as_ref())), "title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())), + "alt" => self.0.push_str(&format!(" alt=\"{}\"", attr.value.as_ref())), _ => {}, } - } - self.0.push_str(" rel=\"nofollow noreferrer\" target=\"_blank\""); - }, - _ => {}, + }, + "a" => { + let any_attr = !tag.attrs.is_empty(); + for attr in tag.attrs { + match attr.name.local.as_ref() { + "href" => self.0.push_str(&format!(" href=\"{}\"", attr.value.as_ref())), + "title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())), + _ => {}, + } + } + if any_attr { + self.0.push_str(" rel=\"nofollow noreferrer\" target=\"_blank\""); + } + }, + _ => {}, + } } if tag.self_closing { self.0.push('/'); } + self.0.push('>'); }, Token::CharacterTokens(txt) => self.0.push_str(txt.as_ref()), diff --git a/utils/uriproxy/src/lib.rs b/utils/uriproxy/src/lib.rs index 87e76a2..cd807f7 100644 --- a/utils/uriproxy/src/lib.rs +++ b/utils/uriproxy/src/lib.rs @@ -2,7 +2,7 @@ use base64::Engine; #[derive(Clone, Copy)] pub enum UriClass { - User, + Actor, Object, Activity, Context, @@ -11,7 +11,7 @@ pub enum UriClass { impl AsRef for UriClass { fn as_ref(&self) -> &str { match self { - Self::User => "users", + Self::Actor => "actors", Self::Object => "objects", Self::Activity => "activities", Self::Context => "context", @@ -38,10 +38,10 @@ pub fn uri(base: &str, entity: UriClass, id: &str) -> String { /// decompose local id constructed by uri() fn pub fn decompose_id(full_id: &str) -> String { - full_id // https://example.org/users/test/followers/page?offset=42 + full_id // https://example.org/actors/test/followers/page?offset=42 .replace("https://", "") .replace("http://", "") - .split('/') // ['example.org', 'users', 'test', 'followers', 'page?offset=42' ] + .split('/') // ['example.org', 'actors', 'test', 'followers', 'page?offset=42' ] .nth(2) // 'test' .unwrap_or("") .to_string() diff --git a/web/index.html b/web/index.html index b4d81ec..a71a7d5 100644 --- a/web/index.html +++ b/web/index.html @@ -9,7 +9,7 @@ - + diff --git a/web/src/app.rs b/web/src/app.rs index 920262b..7882d0d 100644 --- a/web/src/app.rs +++ b/web/src/app.rs @@ -1,19 +1,16 @@ use leptos::*; use leptos_router::*; +use reqwest::Method; use crate::prelude::*; -use leptos_use::{storage::use_local_storage, use_cookie, use_cookie_with_options, utils::{FromToStringCodec, JsonCodec}, UseCookieOptions}; +use leptos_use::{storage::use_local_storage, use_cookie, utils::{FromToStringCodec, JsonCodec}}; #[component] pub fn App() -> impl IntoView { - let (token, set_token) = use_cookie_with_options::( - "token", - UseCookieOptions::default() - .max_age(1000 * 60 * 60 * 6) - ); - let (config, set_config, _) = use_local_storage::("config"); + let (token, set_token) = use_cookie::("token"); let (userid, set_userid) = use_cookie::("user_id"); + let (config, set_config, _) = use_local_storage::("config"); let auth = Auth { token, userid }; provide_context(auth); @@ -22,10 +19,12 @@ pub fn App() -> impl IntoView { let username = auth.userid.get_untracked() .map(|x| x.split('/').last().unwrap_or_default().to_string()) .unwrap_or_default(); - let home_tl = Timeline::new(format!("{URL_BASE}/users/{username}/inbox/page")); + let home_tl = Timeline::new(format!("{URL_BASE}/actors/{username}/inbox/page")); + let user_tl = Timeline::new(format!("{URL_BASE}/actors/{username}/outbox/page")); let server_tl = Timeline::new(format!("{URL_BASE}/inbox/page")); - let user_tl = Timeline::new(format!("{URL_BASE}/users/{username}/outbox/page")); - let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page")); + let local_tl = Timeline::new(format!("{URL_BASE}/outbox/page")); + + let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page")); // TODO ehhh let reply_controls = ReplyControls::default(); provide_context(reply_controls); @@ -35,26 +34,42 @@ pub fn App() -> impl IntoView { let (menu, set_menu) = create_signal(screen_width <= 786); let (advanced, set_advanced) = create_signal(false); - spawn_local(async move { - if let Err(e) = server_tl.more(auth).await { - tracing::error!("error populating timeline: {e}"); - } - }); + let title_target = move || if auth.present() { "/web/home" } else { "/web/server" }; - let auth_present = auth.token.get_untracked().is_some(); // skip helper to use get_untracked - if auth_present { + if let Some(tok) = token.get_untracked() { spawn_local(async move { - if let Err(e) = home_tl.more(auth).await { - tracing::error!("error populating timeline: {e}"); + // refresh token first, or verify that we're still authed + match reqwest::Client::new() + .request(Method::PATCH, format!("{URL_BASE}/auth")) + .json(&serde_json::json!({"token": tok})) + .send() + .await + { + Err(e) => tracing::error!("could not refresh token: {e}"), + Ok(res) => match res.error_for_status() { + Err(e) => tracing::error!("server rejected refresh: {e}"), + Ok(doc) => match doc.json::().await { + Err(e) => tracing::error!("failed parsing auth response: {e}"), + Ok(auth) => { + set_token.set(Some(auth.token)); + set_userid.set(Some(auth.user)); + }, + } + } } - }); - } - let title_target = if auth_present { "/web/home" } else { "/web/server" }; + server_tl.more(auth); + local_tl.more(auth); + if auth.token.get_untracked().is_some() { home_tl.more(auth) }; + }) + } else { + server_tl.more(auth); + local_tl.more(auth); + } view! {