diff --git a/.gitignore b/.gitignore index bca6f9b..ee4e61b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,23 @@ debug/ target/ +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + # These are backup files generated by rustfmt **/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Development +/docker/* +!/docker/.gitkeep +!/docker/postgre +.env +access_token.txt +dump.sql + +# System Specific +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c9657b1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to Rust Commune + +There are many ways to contribute to Commune Rust, including writing code, +openning issues, helping people, reproduce, or fix bugs that people have filed +and improving documentation. + +## Development Environment + +Commune Rust is written in The Rust Programming Language, you will have to +setup Rust in your machine to run the project locally. + +Tools like [**Justfile**][justfile] are recommended to improve DX and reduce +learning curve by running commands easily. + +[docker]: https://www.docker.com/get-started/ +[justfile]: https://github.com/casey/just +[rust]: https://rustup.rs + + +### Getting Started + +1. Create a copy of `.env.example` on `.env` + +```bash +cp .env.example .env +``` + +2. Generate `Synapse` server configuration + +```bash +just gen_synapse_conf +``` + +3. Run Synapse Server (and other containerized services) using Docker Compose +via: + +```bash +just backend +``` + +**When you are ready** + +Teardown services using `just stop`. +If you want to perform a complete cleanup use `just clear`. + +> **Warning** `just clear` will remove all containers and images. + +### Testing + +This application has 2 layers for tests: + +- `Unit`: Are usually inlined inside crates, and dont depend on any integration +- `E2E`: Lives in `test` crate and counts with the services that run the application + +#### Unit + +Unit tests can be executed via `cargo test -p `, this will run +every unit test. + +#### E2E + +You must run Docker services as for development. In order to avoid messing up +the development environment, its recommended to use the synapse setup from +`crates/test/fixtures/synapse` replacing it with `docker/synapse`. + +> Make sure the `.env` file is created from the contents on `.env.example` + +### Application Layout + +
+ + Application Layout Overview +
+ +The client, any HTTP Client, comunicates with the Commune Server which may or +may not communicate with Matrix's server _Synapse_ which runs along with its +database in a Docker container. + +#### Email Development + +Use [MJML Editor][mjml] and then render into HTML. Make sure variables use +Handlebars syntax (e.g. `{{name}}`). + +For local testing you can use something like: + +```bash +curl -s http://localhost:1080/email | grep -o -E "This is your verification code.{0,7}" | tail -1 | sed 's/^.*://' | awk '{$1=$1;print} +``` + +To get the very last email's verification code. + +> **Warning** Note that changes on email content will break this script + +[mjml]: https://mjml.io/try-it-live/99k8regCo_ + +#### Redis + +A Redis instance is used to keep in-memory short-lived data used certain server +operations such as storing verification codes. + +For this purpose Redis is served as part of the development stack on Docker. + +The `redis/redis-stack` image contains both Redis Stack server and RedisInsight, +you can use RedisInsight by pointing your browser to `localhost:8001`. + +#### Synapse + +There is an official [Synapse][1] image available at https://hub.docker.com/r/matrixdotorg/synapse +or at `ghcr.io/matrix-org/synapse` which can be used with the `docker-compose` +file available at [contrib/docker][2]. Further information on this including +configuration options is available in the README on hub.docker.com. + +[1]: https://matrix-org.github.io/synapse/latest/setup/installation.html#docker-images-and-ansible-playbooks +[2]: https://github.com/matrix-org/synapse/tree/develop/contrib/docker diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 93ad2ee..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,691 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "async-trait" -version = "0.1.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "axum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" -dependencies = [ - "async-trait", - "axum-core", - "axum-macros", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bytes" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" - -[[package]] -name = "cc" -version = "1.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "commune-rs" -version = "0.1.0" -dependencies = [ - "axum", - "serde", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys", -] - -[[package]] -name = "object" -version = "0.36.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "serde" -version = "1.0.209" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.209" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "syn" -version = "2.0.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - -[[package]] -name = "tokio" -version = "1.39.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" -dependencies = [ - "backtrace", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 7df2629..bbdf33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,47 +1,70 @@ -[package] -name = "commune-rs" -edition = "2021" -version = "0.1.0" -readme = "README.md" -license = "Apache-2.0" - +[workspace.package] description = "Rust implementation of the Commune server." +edition = "2018" homepage = "https://commune.sh" +license = "Apache-2.0" +name = "commune" +readme = "README.md" repository = "https://github.com/commune-os/commune-rs" +rust-version = "1.75.0" -keywords = [ - "matrix", - "chat", - "messaging", - "federation", - "social", - "community", -] - -[dependencies] -axum = "0.7.5" -serde = { version = "1.0.209", features = ["derive"] } - -[dev-dependencies] -axum = { version = "0.7.5", features = ["macros"] } - -[profile.dev] -incremental = true -opt-level = 1 -lto = 'off' +[workspace] +members = ["crates/core", "crates/matrix", "crates/router", "crates/test"] +default-members = ["crates/router"] +resolver = "2" -# NOTE: you might have to adjust the value for opt-level, as it -# comes with the drawback of less useful error messages for dependencies. -[profile.dev.package."*"] -opt-level = 3 +[workspace.dependencies] +axum-extra = { version = "0.9.3", features = ["typed-header"] } +async-trait = "0.1.74" +# async-stream = "0.3.5" +bytes = "1.5.0" +email_address = { version = "0.2.4", features = ["serde", "serde_support"] } +figment = { version = "0.10.14", features = ["toml", "env"] } +hex = "0.4.3" +tokio-rustls = "0.25.0" +# futures = "0.3.30" +hmac = "0.12.1" +sha1 = "0.10.6" +anyhow = "1.0.75" +axum = { version = "0.7.4", features = ["tokio", "macros"] } +http = "0.2.11" +mime = "0.3.17" +mail-send = "0.4.7" +maud = "0.26.0" +headers = "0.4.0" +# openssl = { version = "0.10.63", features = ["vendored"] } +# openssl-sys = { version = "0.9.99", features = ["vendored"] } +reqwest = { version = "0.11.22", default-features = false, features = [ + "json", + "multipart", + "rustls", +] } +serde = "1.0.192" +serde_json = "1.0.114" +time = "0.3.34" +tokio = "1.34.0" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +url = "2.4.1" +rand = "0.8.5" +thiserror = "1.0.50" +validator = { version = "0.16", features = ["derive"] } -[profile.release] -incremental = true -opt-level = 3 -lto = 'thin' +router = { workspace = true, path = "crates/router" } +matrix = { workspace = true, path = "crates/matrix" } +commune = { workspace = true, path = "crates/core" } -[profile.release.package."*"] -opt-level = 3 +ruma-events = { version = "0.27.11", default_features = false, features = [ + "html", + "markdown", +] } +ruma-common = { version = "0.12.0", default_features = false, features = [ + "api", + "rand", +] } +ruma-macros = { version = "0.12.0", default_features = false } +ruma-client = { version = "0.12.0", default_features = false } +ruma-identifiers-validation = { version = "0.9.3", default_features = false } [workspace.lints.rust] unreachable_pub = "warn" @@ -66,3 +89,17 @@ unused_async = "warn" unused_results = "warn" unwrap_used = "warn" wildcard_imports = "warn" + +[profile.dev] +opt-level = 1 +incremental = true +lto = 'off' + +[profile.release] +lto = 'thin' +incremental = true + +[profile.release.build-override] +opt-level = 3 +[profile.release.package."*"] +opt-level = 3 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7961240 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM arm64v8/alpine:3 + +COPY ./tmp/server /opt/commune + +WORKDIR app + +ENTRYPOINT ["/opt/commune"] diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..a7f5e8b --- /dev/null +++ b/Justfile @@ -0,0 +1,88 @@ +set positional-arguments + +commit_sha := `git rev-parse --verify --short=7 HEAD` +target_release := "x86_64-unknown-linux-musl" + +# Lists all available commands +default: + just --list + +# Creates the `.env` file if it doesn't exist +# This indicates the first invocation of `just` so we also +# create the docker folders while we're at it +dotenv: + export DOCKER_USER="$(id -u):$(id -g)" && \ + cp -n .env.example .env || true && \ + mkdir -p docker/synapse || true + +# Dump database to a file +backup_db: + docker compose exec -T synapse_database \ + pg_dumpall -c -U synapse_user > ./dump.sql + +# Restore database from a file +restore_db: + cat ./dump.sql | docker compose exec -T synapse_database \ + psql -U synapse_user -d synapse + +# Nuke database +nuke_db: + docker compose exec -T synapse_database \ + psql -U synapse_user -d synapse -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" + +# Generates the synapse configuration file and saves it +gen_synapse_conf: dotenv + docker run -i --rm \ + -u "$(id -u):$(id -g)" \ + -v ./docker/synapse:/data \ + --env-file .env \ + matrixdotorg/synapse:v1.96.1 generate + +# Generates a de-facto admin user +gen_synapse_admin: dotenv + docker compose exec -i synapse \ + register_new_matrix_user http://localhost:8008 \ + -c /data/homeserver.yaml \ + -u admin \ + -p admin \ + -a + +# Retrieves admin access token uses de-facto admin user and Development Database Credentials +get_access_token: + sed -i "s/COMMUNE_SYNAPSE_ADMIN_TOKEN='.*'/COMMUNE_SYNAPSE_ADMIN_TOKEN='$( \ + curl -sS -d '{"type":"m.login.password", "user":"admin", "password":"admin"}' \ + http://localhost:8008/_matrix/client/v3/login | jq --raw-output '.access_token' \ + )'/" .env + +# Runs backend dependency services +backend *args='': dotenv + docker compose up --build $1 + +# Stops backend dependency services +stop: + docker compose down + +# Removes oll Docker related config, volumes and containers for this project +clear: stop + docker compose rm --all --force --volumes --stop + docker volume rm commune_synapse_database || true + +# Runs all the tests from the `test` package. Optionally runs a single one if name pattern is provided +e2e *args='': + cargo test --package test -- --nocapture --test-threads=1 $1 + +# Builds the Server binary used in the Docker Image +docker_build_server: + cargo zigbuild --target {{target_release}} --release -p server + +# Builds the Docker image for the backend +docker_build_image: docker_build_server + mkdir tmp/ + cp ./target/{{target_release}}/release/server ./tmp/server + chmod +x ./tmp/server + docker build -t "commune:{{commit_sha}}-{{target_release}}" . + +# Publishes the Docker image to the GitHub Container Registry +docker_publish_image: + docker tag commune:{{commit_sha}}-{{target_release}} ghcr.io/commune-os/commune:{{commit_sha}}-{{target_release}} + docker push ghcr.io/commune-os/commune:{{commit_sha}}-{{target_release}} diff --git a/commune-example.toml b/commune-example.toml new file mode 100644 index 0000000..38f633e --- /dev/null +++ b/commune-example.toml @@ -0,0 +1,23 @@ +registration_verification = false +public_loopback = false +port = 6421 +tls = true + +# Either one works but not both +blocked_domains = [] +# allowed_domains = ['gmail.com', 'outlook.com'] + +# `X-Forwarded-For` header +# xff = false + +[matrix] +server_name = "matrix.localhost" +host = "http://0.0.0.0:8008" +admin_token = "syt_YWRtaW4_FllbTksPWcQaDRUVVcYR_3LJQZ2" +shared_registration_secret = "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B" + +[mail] +host = "smtp://0.0.0.0:1025" +username = "" +password = "" +tls = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 0000000..a6961dc --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "core" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +name = "commune" +path = "src/lib.rs" + +[dependencies] +# Workspace Dependencies +anyhow = { workspace = true } +axum = { workspace = true } +rand = { workspace = true } +email_address = { workspace = true } +thiserror = { workspace = true } +validator = { workspace = true, features = ["derive"] } +http = { workspace = true } +mail-send = { workspace = true } +maud = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } +figment = { workspace = true } +url = { workspace = true, features = ["serde"] } +tokio = { workspace = true, features = ["full"] } +headers = { workspace = true } +tokio-rustls = { workspace = true } + +# Local Dependencies +matrix = { path = "../matrix", features = ["client"] } diff --git a/crates/core/src/account.rs b/crates/core/src/account.rs new file mode 100644 index 0000000..ce3b5ea --- /dev/null +++ b/crates/core/src/account.rs @@ -0,0 +1,8 @@ +pub mod email; +pub mod login; +pub mod logout; +pub mod password; +pub mod register; +pub mod token; +pub mod username; +pub mod whoami; diff --git a/crates/core/src/account/email.rs b/crates/core/src/account/email.rs new file mode 100644 index 0000000..c251b8e --- /dev/null +++ b/crates/core/src/account/email.rs @@ -0,0 +1,32 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use email_address::EmailAddress; +use matrix::admin::registration_tokens::new::*; +use rand::{distributions::Uniform, prelude::Distribution}; + +use crate::{commune, error::Result}; + +pub async fn service(address: EmailAddress) -> Result<()> { + let uni = Uniform::new('0', '9'); + let token: String = uni.sample_iter(rand::thread_rng()).take(6).collect(); + + let req = Request::new( + token.clone(), + 1, + SystemTime::now() + .duration_since(UNIX_EPOCH) + // panics below should never happen + .expect("system time overflow") + .as_millis() + .try_into() + .expect("system time overflow"), + ); + + commune() + .send_matrix_request(req, Some(&commune().config.matrix.admin_token.inner())) + .await?; + + commune().send_email_verification(address, token).await?; + + Ok(()) +} diff --git a/crates/core/src/account/login.rs b/crates/core/src/account/login.rs new file mode 100644 index 0000000..7a31e83 --- /dev/null +++ b/crates/core/src/account/login.rs @@ -0,0 +1,21 @@ +use matrix::client::{login::*, uiaa::UserIdentifier}; + +use crate::{commune, error::Result, util::secret::Secret}; + +pub async fn service(username: impl Into, password: &Secret) -> Result { + let req = Request::new( + LoginType::Password { + password: password.inner(), + }, + Some(UserIdentifier::User { + user: username.into(), + }), + "commune".to_owned(), + Some(true), + ); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/logout.rs b/crates/core/src/account/logout.rs new file mode 100644 index 0000000..972d9ef --- /dev/null +++ b/crates/core/src/account/logout.rs @@ -0,0 +1,12 @@ +use matrix::client::logout::root::*; + +use crate::{commune, error::Result}; + +pub async fn service(access_token: impl AsRef) -> Result { + let req = Request::new(); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/password.rs b/crates/core/src/account/password.rs new file mode 100644 index 0000000..7e7783a --- /dev/null +++ b/crates/core/src/account/password.rs @@ -0,0 +1,20 @@ +use matrix::{client::account::password::*, ruma_common::UserId}; + +use crate::{commune, error::Result, util::secret::Secret}; + +pub async fn service( + access_token: impl AsRef, + username: impl Into, + old_password: Secret, + new_password: Secret, +) -> Result { + let server_name = &crate::commune().config.matrix.server_name; + let user_id = UserId::parse_with_server_name(username.into(), server_name)?; + + let req = Request::new(new_password.inner()).with_password(user_id, old_password.inner()); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/register.rs b/crates/core/src/account/register.rs new file mode 100644 index 0000000..b8b384e --- /dev/null +++ b/crates/core/src/account/register.rs @@ -0,0 +1,53 @@ +use http::StatusCode; +use matrix::{ + client::{ + register::root::*, + uiaa::{Auth, AuthData, AuthType, Dummy, UiaaResponse}, + }, + ruma_client::Error::FromHttpResponse, + ruma_common::api::error::{FromHttpResponseError, MatrixError, MatrixErrorBody}, +}; + +use crate::{commune, error::Result, util::secret::Secret}; + +pub async fn service(username: impl Into, password: Secret) -> Result { + let req = Request::new( + username.into(), + password.inner(), + Some("commune".to_owned()), + None, + None, + ); + + let mut retry_req = req.clone(); + + match commune().send_matrix_request(req, None).await { + Ok(resp) => Ok(resp), + Err(e) => match e { + FromHttpResponse(FromHttpResponseError::Server(MatrixError { + status_code: StatusCode::UNAUTHORIZED, + body: MatrixErrorBody::Json(ref body), + })) => { + let UiaaResponse { flows, session, .. } = + serde_json::from_value::(body.clone()).unwrap(); + + match flows.as_slice() { + [value] => match value.stages.as_slice() { + [AuthType::Dummy] => { + retry_req.auth = Some(Auth::new(AuthData::Dummy(Dummy {}), session)); + + commune() + .send_matrix_request(retry_req, None) + .await + .map_err(Into::into) + } + _ => Err(e.into()), + }, + _ => Err(e.into()), + } + } + + _ => Err(e.into()), + }, + } +} diff --git a/crates/core/src/account/token.rs b/crates/core/src/account/token.rs new file mode 100644 index 0000000..cc85ac4 --- /dev/null +++ b/crates/core/src/account/token.rs @@ -0,0 +1,12 @@ +use matrix::client::register::token::validity::*; + +use crate::{commune, error::Result}; + +pub async fn service(access_token: impl AsRef) -> Result { + let req = Request::new(access_token.as_ref().to_owned()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/username.rs b/crates/core/src/account/username.rs new file mode 100644 index 0000000..37528d7 --- /dev/null +++ b/crates/core/src/account/username.rs @@ -0,0 +1,12 @@ +use matrix::client::register::available::*; + +use crate::{commune, error::Result}; + +pub async fn service(username: impl Into) -> Result { + let req = Request::new(username.into()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/whoami.rs b/crates/core/src/account/whoami.rs new file mode 100644 index 0000000..908e1bc --- /dev/null +++ b/crates/core/src/account/whoami.rs @@ -0,0 +1,12 @@ +use matrix::client::account::whoami::*; + +use crate::{commune, error::Result}; + +pub async fn service(access_token: impl AsRef) -> Result { + let req = Request::new(); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs new file mode 100644 index 0000000..57b5f82 --- /dev/null +++ b/crates/core/src/config.rs @@ -0,0 +1,34 @@ +use matrix::ruma_common::OwnedServerName; +use serde::Deserialize; +use url::Url; + +use crate::util::secret::Secret; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub registration_verification: bool, + pub public_loopback: bool, + pub port: Option, + + pub allowed_domains: Option>, + pub blocked_domains: Option>, + + pub matrix: Matrix, + pub mail: SMTP, +} + +#[derive(Debug, Deserialize)] +pub struct SMTP { + pub host: Url, + pub username: Option, + pub password: Secret, + pub tls: bool, +} + +#[derive(Debug, Deserialize)] +pub struct Matrix { + pub host: Url, + pub server_name: OwnedServerName, + pub admin_token: Secret, + pub shared_registration_secret: Secret, +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs new file mode 100644 index 0000000..7134727 --- /dev/null +++ b/crates/core/src/error.rs @@ -0,0 +1,32 @@ +use axum::{http::StatusCode, response::IntoResponse}; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + #[error("forwarding a Matrix request failed: {0}")] + Matrix(#[from] matrix::HandleError), + + #[error("instance does not allow email address originating from this domain")] + EmailDomain, + + #[error("failed to validate identifier: {0}")] + InvalidIdentifier(#[from] matrix::ruma_identifiers_validation::Error), + + #[error("an IO operation failed: {0}")] + IO(#[from] std::io::Error), + + #[error(transparent)] + SMTP(#[from] mail_send::Error), + + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 0000000..2ccd121 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,128 @@ +//! This library deals with our core logic, such as authorizing user +//! interactions, forwarding regular events and constructing custom requests. + +pub mod config; +pub mod error; +pub mod util; + +pub mod account; +pub mod profile; + +use std::sync::RwLock; + +use config::Config; +use email_address::EmailAddress; +use figment::{ + providers::{Env, Format, Toml}, + Figment, +}; +use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder}; +use matrix::{ + ruma_client::{HttpClientExt, ResponseResult}, + ruma_common::api::{OutgoingRequest, SendAccessToken}, +}; + +static COMMUNE: RwLock> = RwLock::new(None); + +pub struct Commune { + pub config: Config, + client: matrix::Client, + // smtp: SmtpClient>, +} + +pub async fn init() { + let mut commune = COMMUNE.write().unwrap(); + + let config = Figment::new() + .merge(Toml::file( + Env::var("COMMUNE_CONFIG").unwrap_or("./commune-example.toml".to_owned()), + )) + .extract::() + .unwrap(); + + if config + .allowed_domains + .as_ref() + .is_some_and(|v| !v.is_empty()) + && config + .blocked_domains + .as_ref() + .is_some_and(|v| !v.is_empty()) + { + panic!("config can only contain either allowed or blocked domains"); + } + + let client = matrix::Client::default(); + + *commune = Some(Box::leak(Box::new(Commune { config, client }))); +} + +pub fn commune() -> &'static Commune { + COMMUNE + .read() + .unwrap() + .expect("commune should be initialized at this point") +} + +impl Commune { + pub async fn send_matrix_request( + &self, + request: R, + access_token: Option<&str>, + ) -> ResponseResult { + let at = match access_token { + Some(at) => SendAccessToken::Always(at), + None => SendAccessToken::None, + }; + + self.client + .send_matrix_request::(self.config.matrix.host.as_str(), at, &[], request) + .await + } + + pub async fn send_email_verification( + &self, + address: EmailAddress, + token: impl Into, + ) -> mail_send::Result<()> { + let config = &commune().config; + + let password = config.mail.password.inner(); + let username = config + .mail + .username + .as_deref() + .unwrap_or(&password) + .to_owned(); + let host = &config.mail.host; + + let mut smtp = SmtpClientBuilder::new( + host.host_str() + .expect("failed to extract host from email configuration"), + 587, + ) + .implicit_tls(false) + .credentials((username.as_str(), password.as_str())) + .connect() + .await?; + + let token = token.into(); + let from = format!("commune@{host}"); + let html = format!( + "

Thanks for signing up.\n\nUse this code to finish verifying your \ + email:\n{token}

" + ); + let text = format!( + "Thanks for signing up.\n\nUse this code to finish verifying your email:\n{token}" + ); + + let message = MessageBuilder::new() + .from(("Commune", from.as_str())) + .to(vec![address.as_str()]) + .subject("Email Verification Code") + .html_body(html.as_str()) + .text_body(text.as_str()); + + smtp.send(message).await + } +} diff --git a/crates/core/src/profile.rs b/crates/core/src/profile.rs new file mode 100644 index 0000000..bea0178 --- /dev/null +++ b/crates/core/src/profile.rs @@ -0,0 +1,2 @@ +pub mod avatar; +pub mod display_name; diff --git a/crates/core/src/profile/avatar.rs b/crates/core/src/profile/avatar.rs new file mode 100644 index 0000000..078c6cb --- /dev/null +++ b/crates/core/src/profile/avatar.rs @@ -0,0 +1,41 @@ +pub mod get { + use matrix::{client::profile::avatar_url::get::*, ruma_common::OwnedUserId}; + + use crate::{commune, error::Result}; + + pub async fn service(user_id: impl Into) -> Result { + let req = Request::new(user_id.into()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) + } +} + +pub mod update { + use matrix::{ + client::{account::whoami, profile::avatar_url::update::*}, + ruma_common::OwnedMxcUri, + }; + + use crate::{commune, error::Result}; + + pub async fn service( + access_token: impl AsRef, + mxc_uri: impl Into, + ) -> Result { + let req = whoami::Request::new(); + + let whoami::Response { user_id, .. } = commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let req = Request::new(user_id, mxc_uri.into()); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) + } +} diff --git a/crates/core/src/profile/display_name.rs b/crates/core/src/profile/display_name.rs new file mode 100644 index 0000000..730ac70 --- /dev/null +++ b/crates/core/src/profile/display_name.rs @@ -0,0 +1,38 @@ +pub mod get { + use matrix::{client::profile::display_name::get::*, ruma_common::OwnedUserId}; + + use crate::{commune, error::Result}; + + pub async fn service(user_id: impl Into) -> Result { + let req = Request::new(user_id.into()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) + } +} + +pub mod update { + use matrix::client::{account::whoami, profile::display_name::update::*}; + + use crate::{commune, error::Result}; + + pub async fn service( + access_token: impl AsRef, + display_name: impl Into, + ) -> Result { + let req = whoami::Request::new(); + + let whoami::Response { user_id, .. } = commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let req = Request::new(user_id, display_name.into()); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) + } +} diff --git a/crates/core/src/util.rs b/crates/core/src/util.rs new file mode 100644 index 0000000..73b12db --- /dev/null +++ b/crates/core/src/util.rs @@ -0,0 +1 @@ +pub mod secret; diff --git a/crates/core/src/util/secret.rs b/crates/core/src/util/secret.rs new file mode 100644 index 0000000..b1eb440 --- /dev/null +++ b/crates/core/src/util/secret.rs @@ -0,0 +1,76 @@ +use std::fmt::{Debug, Display}; + +use rand::{distributions::Uniform, Rng}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Secret(String); + +// is this necessary? +impl Serialize for Secret { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner().serialize(serializer) + } +} + +impl Secret { + #[inline] + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + #[inline] + pub fn inner(&self) -> String { + self.0.clone() + } +} + +impl Debug for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("{self}").as_str()) + } +} + +impl Display for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let braille_range = Uniform::new('\u{2800}', '\u{28FF}'); + let s: String = rand::thread_rng() + .sample_iter(braille_range) + .take(self.0.len()) + .collect(); + + f.write_str(s.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn do_not_display_value() { + let secret = Secret::new("secret"); + let display = format!("{}", secret); + + assert_eq!(display, "[REDACTED]"); + } + + #[test] + fn do_not_debug_value() { + let secret = Secret::new("secret"); + let display = format!("{:?}", secret); + + assert_eq!(display, "[REDACTED]"); + } + + #[test] + fn retrieves_original() { + let secret = Secret::new("secret"); + let value = secret.inner(); + + assert_eq!(value, "secret".into()); + } +} diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml new file mode 100644 index 0000000..25a1738 --- /dev/null +++ b/crates/matrix/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "matrix" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +name = "matrix" +path = "src/lib.rs" + +[dependencies] +ruma-events = { workspace = true } +ruma-common = { workspace = true } +ruma-macros = { workspace = true } +ruma-client = { workspace = true } +ruma-identifiers-validation = { workspace = true } + +# Workspace Dependencies +mime = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +sha1 = { workspace = true } +url = { workspace = true, features = ["serde"] } +hex = { workspace = true } +hmac = { workspace = true } +http = { workspace = true } +bytes = { workspace = true } +async-trait = { workspace = true } + +[features] +client = [] +server = [] diff --git a/crates/matrix/src/admin.rs b/crates/matrix/src/admin.rs new file mode 100644 index 0000000..2608ca2 --- /dev/null +++ b/crates/matrix/src/admin.rs @@ -0,0 +1,8 @@ +//! This module is the root of the admin API. +//! +//! reference: https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html + +pub mod registration_tokens; +// pub mod room; +// pub mod session; +// pub mod user; diff --git a/crates/matrix/src/admin/registration_tokens.rs b/crates/matrix/src/admin/registration_tokens.rs new file mode 100644 index 0000000..9d52a2b --- /dev/null +++ b/crates/matrix/src/admin/registration_tokens.rs @@ -0,0 +1 @@ +pub mod new; diff --git a/crates/matrix/src/admin/registration_tokens/new.rs b/crates/matrix/src/admin/registration_tokens/new.rs new file mode 100644 index 0000000..7849c07 --- /dev/null +++ b/crates/matrix/src/admin/registration_tokens/new.rs @@ -0,0 +1,38 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register/new", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub token: String, + + pub uses_allowed: usize, + + pub expiry_time: usize, +} + +impl Request { + pub fn new(token: String, uses_allowed: usize, expiry_time: usize) -> Self { + Self { + token, + uses_allowed, + expiry_time, + } + } +} + +// Same fields as above are returned but we only +// care about knowing whether the call was successful. +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/admin/room.rs b/crates/matrix/src/admin/room.rs new file mode 100644 index 0000000..38a3738 --- /dev/null +++ b/crates/matrix/src/admin/room.rs @@ -0,0 +1,59 @@ +//! This module contains handlers for managing rooms. +//! +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html + +use ruma_common::{ + room::RoomType, EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, + OwnedUserId, RoomVersionId, +}; +use ruma_events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}; +use serde::Deserialize; + +pub mod delete_room; +pub mod get_members; +pub mod get_room; +pub mod get_rooms; +pub mod get_state; + +#[derive(Clone, Debug, Deserialize)] +pub struct Room { + pub room_id: OwnedRoomId, + + pub canonical_alias: Option, + + pub avatar: Option, + + pub name: Option, + + pub joined_members: u64, + + pub joined_local_members: u64, + + pub version: RoomVersionId, + + pub creator: OwnedUserId, + + pub encryption: Option, + + pub federatable: bool, + + pub public: bool, + + pub join_rules: Option, + + pub history_visibility: Option, + + pub state_events: u64, + + pub room_type: Option, + + #[serde(flatten)] + pub details: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RoomDetails { + pub topic: Option, + + pub forgotten: bool, +} diff --git a/crates/matrix/src/admin/room/delete_room.rs b/crates/matrix/src/admin/room/delete_room.rs new file mode 100644 index 0000000..578a334 --- /dev/null +++ b/crates/matrix/src/admin/room/delete_room.rs @@ -0,0 +1,47 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: DELETE, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/rooms/:room_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub new_room: Option, + + pub block: bool, + + #[serde(skip_serializing_if = "ruma_common::serde::is_true")] + pub purge: bool, + + pub force_purge: bool, +} + +#[response(error = crate::Error)] +pub struct Response { + pub delete_id: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NewRoomParams { + pub creator: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub message: String, +} diff --git a/crates/matrix/src/admin/room/forward_extremities/delete.rs b/crates/matrix/src/admin/room/forward_extremities/delete.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/room/forward_extremities/get.rs b/crates/matrix/src/admin/room/forward_extremities/get.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/room/get_members.rs b/crates/matrix/src/admin/room/get_members.rs new file mode 100644 index 0000000..79cd4e7 --- /dev/null +++ b/crates/matrix/src/admin/room/get_members.rs @@ -0,0 +1,27 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id/members", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + pub members: Vec, + + pub total: u64, +} diff --git a/crates/matrix/src/admin/room/get_room.rs b/crates/matrix/src/admin/room/get_room.rs new file mode 100644 index 0000000..967f577 --- /dev/null +++ b/crates/matrix/src/admin/room/get_room.rs @@ -0,0 +1,27 @@ +use super::Room; +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub room: Room, +} diff --git a/crates/matrix/src/admin/room/get_rooms.rs b/crates/matrix/src/admin/room/get_rooms.rs new file mode 100644 index 0000000..08a792c --- /dev/null +++ b/crates/matrix/src/admin/room/get_rooms.rs @@ -0,0 +1,83 @@ +use ruma_common::{ + api::{request, response, Direction, Metadata}, + metadata, +}; +use serde::Serialize; + +use super::Room; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(default)] + #[ruma_api(query)] + pub from: u64, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + + #[ruma_api(query)] + pub order_by: OrderBy, + + #[ruma_api(query)] + pub direction: Direction, + + #[serde(skip_serializing_if = "String::is_empty")] + #[ruma_api(query)] + pub search_term: String, +} + +#[response(error = crate::Error)] +pub struct Response { + pub rooms: Vec, + + pub offset: u64, + + #[serde(rename = "total_rooms")] + pub total: u64, + + pub next_batch: Option, + + pub prev_batch: Option, +} + +#[derive(Clone, Default, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum OrderBy { + #[default] + Name, + + CanonicalAlias, + + JoinedMembers, + + JoinedLocalMembers, + + Version, + + Creator, + + Encryption, + + Federatable, + + Public, + + JoinRules, + + GuestAccess, + + HistoryVisibility, + + StateEvents, +} diff --git a/crates/matrix/src/admin/room/get_state.rs b/crates/matrix/src/admin/room/get_state.rs new file mode 100644 index 0000000..6cf649b --- /dev/null +++ b/crates/matrix/src/admin/room/get_state.rs @@ -0,0 +1,36 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; +use serde::Deserialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id/state", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + pub state: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct State { + #[serde(rename = "type")] + pub kind: String, + + pub state_key: String, + + pub etc: bool, +} diff --git a/crates/matrix/src/admin/session.rs b/crates/matrix/src/admin/session.rs new file mode 100644 index 0000000..10d725f --- /dev/null +++ b/crates/matrix/src/admin/session.rs @@ -0,0 +1,46 @@ +//! This module contains handlers for user registration. +//! +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/register_api.html + +use hmac::Mac; +use serde::Serialize; + +pub mod get_nonce; +pub mod register; + +#[derive(Clone, Debug, Serialize)] +pub struct Hmac { + inner: Vec, +} + +impl Hmac { + pub fn new( + shared_secret: &str, + nonce: &str, + username: &str, + password: &str, + admin: bool, + ) -> Result { + let mut mac = hmac::Hmac::::new_from_slice(shared_secret.as_bytes())?; + let admin = match admin { + true => "admin", + false => "notadmin", + }; + + mac.update( + &[nonce, username, password, admin] + .map(str::as_bytes) + .join(&0x00), + ); + + let result = mac.finalize().into_bytes(); + + Ok(Self { + inner: result.to_vec(), + }) + } + + pub fn get(&self) -> String { + hex::encode(&self.inner) + } +} diff --git a/crates/matrix/src/admin/session/get_nonce.rs b/crates/matrix/src/admin/session/get_nonce.rs new file mode 100644 index 0000000..0388987 --- /dev/null +++ b/crates/matrix/src/admin/session/get_nonce.rs @@ -0,0 +1,22 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[response(error = crate::Error)] +pub struct Response { + pub nonce: String, +} diff --git a/crates/matrix/src/admin/session/register.rs b/crates/matrix/src/admin/session/register.rs new file mode 100644 index 0000000..4838353 --- /dev/null +++ b/crates/matrix/src/admin/session/register.rs @@ -0,0 +1,43 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedServerName, OwnedUserId, +}; + +use super::Hmac; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub nonce: String, + + pub username: String, + + pub password: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub displayname: String, + + pub admin: bool, + + pub hmac: Hmac, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub user_id: OwnedUserId, + + pub home_server: OwnedServerName, + + pub device_id: OwnedDeviceId, +} diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs new file mode 100644 index 0000000..3417bb5 --- /dev/null +++ b/crates/matrix/src/admin/user.rs @@ -0,0 +1,53 @@ +//! This module contains handlers for managing users. +//! +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html + +use ruma_common::{thirdparty::ThirdPartyIdentifier, OwnedMxcUri, OwnedUserId}; +use serde::{Deserialize, Serialize}; + +pub mod get_user; +pub mod get_user_by_3pid; +pub mod get_users; +pub mod set_user; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct User { + #[serde(rename = "name")] + pub user_id: OwnedUserId, + + pub displayname: Option, + + pub avatar_url: Option, + + pub threepids: Vec, + + pub external_ids: Vec, + + pub admin: bool, + + pub deactivated: bool, + + #[serde(skip_serializing)] + pub erased: bool, + + #[serde(skip_serializing)] + pub shadow_banned: bool, + + #[serde(skip_serializing)] + pub creation_ts: u64, + + #[serde(skip_serializing)] + pub consent_server_notice_sent: Option, + + #[serde(skip_serializing)] + pub consent_ts: Option, + + pub locked: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ExternalId { + pub auth_provider: String, + + pub external_id: String, +} diff --git a/crates/matrix/src/admin/user/get_user.rs b/crates/matrix/src/admin/user/get_user.rs new file mode 100644 index 0000000..da302be --- /dev/null +++ b/crates/matrix/src/admin/user/get_user.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/users/:user_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub user: User, +} diff --git a/crates/matrix/src/admin/user/get_user_by_3pid.rs b/crates/matrix/src/admin/user/get_user_by_3pid.rs new file mode 100644 index 0000000..2263ba7 --- /dev/null +++ b/crates/matrix/src/admin/user/get_user_by_3pid.rs @@ -0,0 +1,32 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + thirdparty::Medium, +}; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/threepid/:medium/users/:address", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub medium: Medium, + + #[ruma_api(path)] + pub address: String, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub user: User, +} diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs new file mode 100644 index 0000000..ad094b8 --- /dev/null +++ b/crates/matrix/src/admin/user/get_users.rs @@ -0,0 +1,84 @@ +use ruma_common::{ + api::{request, response, Direction, Metadata}, + metadata, OwnedUserId, +}; +use serde::Serialize; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/users", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub user_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub admins: Option, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub deactivated: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub from: u64, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub order_by: OrderBy, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub dir: Direction, +} + +#[response(error = crate::Error)] +pub struct Response { + pub users: Vec, + + pub next_token: String, + + pub total: u64, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +#[allow(dead_code)] +pub enum OrderBy { + #[default] + Name, + + Admin, + + UserType, + + Deactivated, + + ShadowBanned, + + Displayname, + + AvatarUrl, + + CreationTs, + + LastSeenTs, +} diff --git a/crates/matrix/src/admin/user/set_user.rs b/crates/matrix/src/admin/user/set_user.rs new file mode 100644 index 0000000..83868f1 --- /dev/null +++ b/crates/matrix/src/admin/user/set_user.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/users/:user_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[ruma_api(body)] + pub user: User, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client-backup/account.rs.bk.bk b/crates/matrix/src/client-backup/account.rs.bk.bk new file mode 100644 index 0000000..d48afb7 --- /dev/null +++ b/crates/matrix/src/client-backup/account.rs.bk.bk @@ -0,0 +1,3 @@ +pub mod create; +pub mod password; +pub mod whoami; diff --git a/crates/matrix/src/client-backup/events.rs.bk.bk.bk b/crates/matrix/src/client-backup/events.rs.bk.bk.bk new file mode 100644 index 0000000..2953b2b --- /dev/null +++ b/crates/matrix/src/client-backup/events.rs.bk.bk.bk @@ -0,0 +1,310 @@ +use anyhow::Result; +use ruma_common::{serde::Raw, EventId, OwnedEventId, OwnedTransactionId, RoomId}; + +use ruma_events::{ + relation::RelationType, AnyMessageLikeEvent, AnyStateEvent, AnyStateEventContent, + AnyTimelineEvent, MessageLikeEventContent, MessageLikeEventType, StateEventContent, + StateEventType, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tracing::instrument; + +use crate::{admin::resources::room::Direction, error::MatrixError, Client}; + +pub struct EventsService; + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetMessagesQuery { + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + pub dir: Direction, + + #[serde(skip_serializing_if = "String::is_empty")] + pub filter: String, +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetRelationsQuery { + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + pub dir: Direction, +} + +#[derive(Debug, Deserialize)] +pub struct GetMessagesResponse { + pub chunk: Vec>, + pub start: String, + pub end: String, + pub state: Option>>, +} + +#[derive(Debug, Deserialize)] +#[serde(transparent)] +pub struct GetStateResponse(pub Vec>); + +#[derive(Debug, Deserialize)] +pub struct GetRelationsResponse { + pub chunk: Vec>, + pub prev_batch: Option, + pub next_batch: Option, +} + +#[derive(Debug, Default, Serialize)] +pub struct SendRedactionBody { + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[derive(Debug, Deserialize)] +pub struct SendMessageResponse { + pub event_id: OwnedEventId, +} + +#[derive(Debug, Deserialize)] +pub struct SendStateResponse { + pub event_id: OwnedEventId, +} + +#[derive(Debug, Deserialize)] +pub struct SendRedactionResponse { + pub event_id: OwnedEventId, +} + +impl EventsService { + #[instrument(skip(client, access_token))] + pub async fn get_event( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_id: &EventId, + ) -> Result> { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get(format!( + "/_matrix/client/v3/rooms/{room_id}/event/{event_id}", + room_id = room_id, + event_id = event_id, + )) + .await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_messages( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + query: GetMessagesQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get_query( + format!( + "/_matrix/client/v3/rooms/{room_id}/messages", + room_id = room_id, + ), + &query, + ) + .await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_state( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get(format!( + "/_matrix/client/v3/rooms/{room_id}/state", + room_id = room_id, + )) + .await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_state_content( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_type: StateEventType, + state_key: Option, + ) -> Result> { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let mut path = format!( + "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", + room_id = room_id, + event_type = event_type, + ); + + if let Some(state_key) = state_key { + path.push_str(&format!("/{state_key}", state_key = state_key)) + } + + let resp = tmp.get(path).await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_relations( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_id: &EventId, + rel_type: Option>, + event_type: Option, + query: GetRelationsQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let mut path = format!( + "/_matrix/client/v3/rooms/{room_id}/relations/{event_id}", + room_id = room_id, + event_id = event_id, + ); + + if let Some(rel_type) = rel_type { + path.push_str(&format!( + "/{rel_type}", + rel_type = rel_type + .as_ref() + .map_or("m.in_reply_to".into(), ToString::to_string) + )); + + if let Some(event_type) = event_type { + path.push_str(&format!("/{event_type}", event_type = event_type)) + } + } + + let resp = tmp.get_query(path, &query).await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token, body))] + pub async fn send_message( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + txn_id: OwnedTransactionId, + body: T, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .put_json( + format!( + "/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}", + room_id = room_id, + event_type = body.event_type(), + txn_id = txn_id, + ), + &body, + ) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) + } + + #[instrument(skip(client, access_token, body))] + pub async fn send_state( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + state_key: Option, + body: T, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let mut path = format!( + "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", + room_id = room_id, + event_type = body.event_type(), + ); + + if let Some(state_key) = state_key { + path.push_str(&format!("/{state_key}", state_key = state_key)) + } + + let resp = tmp.put_json(path, &body).await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) + } + + #[instrument(skip(client, access_token, body))] + pub async fn send_redaction( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_id: &EventId, + txn_id: OwnedTransactionId, + body: SendRedactionBody, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .put_json( + format!( + "/_matrix/client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}", + room_id = room_id, + event_id = event_id, + txn_id = txn_id, + ), + &body, + ) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) + } +} diff --git a/crates/matrix/src/client-backup/membership.rs.bk.bk b/crates/matrix/src/client-backup/membership.rs.bk.bk new file mode 100644 index 0000000..86c3778 --- /dev/null +++ b/crates/matrix/src/client-backup/membership.rs.bk.bk @@ -0,0 +1,5 @@ +pub mod ban; +pub mod join; +pub mod kick; +pub mod leave; +pub mod unban; diff --git a/crates/matrix/src/client-backup/mod.rs.bk.bk b/crates/matrix/src/client-backup/mod.rs.bk.bk new file mode 100644 index 0000000..cc296e6 --- /dev/null +++ b/crates/matrix/src/client-backup/mod.rs.bk.bk @@ -0,0 +1,10 @@ +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +pub mod rooms; +pub mod session; +pub mod membership; +pub mod uiaa; +pub mod sync; +pub mod account; diff --git a/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk b/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk new file mode 100644 index 0000000..7dc669a --- /dev/null +++ b/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk @@ -0,0 +1,184 @@ +use std::str::FromStr; + +use anyhow::Result; +use mime::Mime; +use ruma_common::{MxcUri, OwnedMxcUri}; +use serde::{de, Deserialize, Deserializer, Serialize}; +use tracing::instrument; + +use chrono::{serde::ts_microseconds_option, DateTime, Utc}; + +use crate::error::MatrixError; + +fn parse_mime_opt<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::<&str>::deserialize(d)? + .map(::from_str) + .transpose() + .map_err(de::Error::custom) +} + +#[derive(Debug, Serialize)] +pub struct GetPreviewUrlQuery { + pub url: url::Url, + pub ts: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateMxcUriResponse { + pub content_uri: String, + + #[serde(with = "ts_microseconds_option")] + pub unused_expires_at: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct GetPreviewUrlResponse { + #[serde(rename = "matrix:image_size")] + pub image_size: Option, + + #[serde(rename = "og:description")] + pub description: Option, + + #[serde(rename = "og:image")] + pub image: Option, + + #[serde(rename = "og:image:height")] + pub height: Option, + + #[serde(rename = "og:image:width")] + pub width: Option, + + #[serde(rename = "og:image:type", deserialize_with = "parse_mime_opt")] + pub kind: Option, + + #[serde(rename = "og:title")] + pub title: Option, +} + +#[derive(Debug, Deserialize)] +pub struct GetConfigResponse { + #[serde(rename = "m.upload.size")] + pub upload_size: Option, +} + +#[derive(Debug, Serialize)] +pub enum ResizeMethod { + Crop, + Scale, +} + +pub struct MxcService; + +#[derive(Debug, Deserialize)] +pub struct MxcError { + #[serde(flatten)] + pub inner: MatrixError, + + pub retry_after_ms: u64, +} + +impl MxcService { + /// Creates a new `MxcUri`, independently of the content being uploaded + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#post_matrixmediav1create + #[instrument(skip(client, access_token))] + pub async fn create( + client: &crate::http::Client, + access_token: impl Into, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp.post("/_matrix/media/v1/create").await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } + + /// Retrieve the configuration of the content repository + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3config + #[instrument(skip(client, access_token))] + pub async fn get_config( + client: &crate::http::Client, + access_token: impl Into, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp.get("/_matrix/media/v3/config").await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } + + /// Retrieve a URL to download content from the content repository, + /// optionally replacing the name of the file. + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3downloadservernamemediaid + #[instrument(skip(client, access_token))] + pub async fn get_download_url( + client: &crate::http::Client, + access_token: impl Into, + mxc_uri: &MxcUri, + mut base_url: url::Url, + file_name: Option, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let (server_name, media_id) = mxc_uri.parts().unwrap(); + + let mut path = format!( + "/_matrix/media/v3/download/{server_name}/{media_id}", + server_name = server_name, + media_id = media_id, + ); + + if let Some(file_name) = file_name { + path.push_str(&format!("/{file_name}", file_name = file_name)) + } + + base_url.set_path(&path); + + Ok(base_url) + } + + /// + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3preview_url + #[instrument(skip(client, access_token))] + pub async fn get_preview( + client: &crate::http::Client, + access_token: impl Into, + query: GetPreviewUrlQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get_query("/_matrix/media/v3/preview_url".to_string(), &query) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } +} diff --git a/crates/matrix/src/client-backup/rooms.rs.bk.bk b/crates/matrix/src/client-backup/rooms.rs.bk.bk new file mode 100644 index 0000000..2d4cdf8 --- /dev/null +++ b/crates/matrix/src/client-backup/rooms.rs.bk.bk @@ -0,0 +1,6 @@ +//! This module contains handlers to interact with rooms. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms + +pub mod create; +pub mod forget; diff --git a/crates/matrix/src/client-backup/session.rs.bk.bk.bk b/crates/matrix/src/client-backup/session.rs.bk.bk.bk new file mode 100644 index 0000000..6bceee3 --- /dev/null +++ b/crates/matrix/src/client-backup/session.rs.bk.bk.bk @@ -0,0 +1,2 @@ +pub mod create; +pub mod invalidate; diff --git a/crates/matrix/src/client-backup/sync.rs.bk.bk b/crates/matrix/src/client-backup/sync.rs.bk.bk new file mode 100644 index 0000000..906f9cf --- /dev/null +++ b/crates/matrix/src/client-backup/sync.rs.bk.bk @@ -0,0 +1,564 @@ +//! This module contains handlers for getting and synchronizing events. +//! +//! reference: https://github.com/matrix-org/matrix-spec-proposals/pull/3575 + +use std::{collections::BTreeMap, time::Duration}; + +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::{deserialize_cow_str, duration::opt_ms, Raw}, + DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, +}; +use ruma_events::{ + receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, + AnyToDeviceEvent, StateEventType, TimelineEventType, +}; +use serde::{self, de::Error as _, Deserialize, Serialize}; + +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/org.matrix.msc3575/sync", + // 1.4 => "/_matrix/client/v4/sync", + } +}; + +#[request(error = crate::Error)] +#[derive(Default)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub pos: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub delta_token: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub conn_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub timeout: Option, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub room_subscriptions: BTreeMap, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub unsubscribe_rooms: Vec, + + #[serde(default, skip_serializing_if = "ExtensionsConfig::is_empty")] + pub extensions: ExtensionsConfig, +} + +#[response(error = crate::Error)] +pub struct Response { + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub initial: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + pub pos: String, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap, + + #[serde(default, skip_serializing_if = "Extensions::is_empty")] + pub extensions: Extensions, + + pub delta_token: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct UnreadNotificationsCount { + #[serde(skip_serializing_if = "Option::is_none")] + pub highlight_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub notification_count: Option, +} + +impl UnreadNotificationsCount { + pub fn is_empty(&self) -> bool { + self.highlight_count.is_none() && self.notification_count.is_none() + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct DeviceLists { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub changed: Vec, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub left: Vec, +} + +impl DeviceLists { + pub fn is_empty(&self) -> bool { + self.changed.is_empty() && self.left.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SyncRequestListFilters { + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub spaces: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_encrypted: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_invite: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_tombstoned: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub room_types: Vec, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_room_types: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub room_name_like: Option, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub tags: Vec, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_tags: Vec, + + #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")] + pub extensions: BTreeMap, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SyncRequestList { + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub slow_get_all_rooms: bool, + + pub ranges: Vec<(usize, usize)>, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sort: Vec, + + #[serde(flatten)] + pub room_details: RoomDetailsConfig, + + #[serde(skip_serializing_if = "Option::is_none")] + pub include_old_rooms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub filters: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub bump_event_types: Vec, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RoomDetailsConfig { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct IncludeOldRooms { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RoomSubscription { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum SlidingOp { + Sync, + + Insert, + + Delete, + + Invalidate, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SyncList { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ops: Vec, + + pub count: usize, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SyncOp { + pub op: SlidingOp, + + pub range: Option<(usize, usize)>, + + pub index: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub room_ids: Vec, + + pub room_id: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct SlidingSyncRoom { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avatar: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub initial: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub invite_state: Option>>, + + #[serde( + flatten, + default, + skip_serializing_if = "UnreadNotificationsCount::is_empty" + )] + pub unread_notifications: UnreadNotificationsCount, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub timeline: Vec>, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub limited: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub joined_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub invited_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub num_live: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ExtensionsConfig { + #[serde(default, skip_serializing_if = "ToDeviceConfig::is_empty")] + pub to_device: ToDeviceConfig, + + #[serde(default, skip_serializing_if = "E2EEConfig::is_empty")] + pub e2ee: E2EEConfig, + + #[serde(default, skip_serializing_if = "AccountDataConfig::is_empty")] + pub account_data: AccountDataConfig, + + #[serde(default, skip_serializing_if = "ReceiptsConfig::is_empty")] + pub receipts: ReceiptsConfig, + + #[serde(default, skip_serializing_if = "TypingConfig::is_empty")] + pub typing: TypingConfig, + + #[serde(flatten)] + other: BTreeMap, +} + +impl ExtensionsConfig { + pub fn is_empty(&self) -> bool { + self.to_device.is_empty() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + && self.other.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Extensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub to_device: Option, + + #[serde(default, skip_serializing_if = "E2EE::is_empty")] + pub e2ee: E2EE, + + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + #[serde(default, skip_serializing_if = "Receipts::is_empty")] + pub receipts: Receipts, + + #[serde(default, skip_serializing_if = "Typing::is_empty")] + pub typing: Typing, +} + +impl Extensions { + pub fn is_empty(&self) -> bool { + self.to_device.is_none() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ToDeviceConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub since: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl ToDeviceConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() && self.limit.is_none() && self.since.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct ToDevice { + pub next_batch: String, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct E2EEConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} + +impl E2EEConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct E2EE { + #[serde(default, skip_serializing_if = "DeviceLists::is_empty")] + pub device_lists: DeviceLists, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub device_one_time_keys_count: BTreeMap, + + #[serde(skip_serializing_if = "Option::is_none")] + pub device_unused_fallback_key_types: Option>, +} + +impl E2EE { + pub fn is_empty(&self) -> bool { + self.device_lists.is_empty() + && self.device_one_time_keys_count.is_empty() + && self.device_unused_fallback_key_types.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct AccountDataConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl AccountDataConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct AccountData { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub global: Vec>, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>>, +} + +impl AccountData { + pub fn is_empty(&self) -> bool { + self.global.is_empty() && self.rooms.is_empty() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RoomReceiptConfig { + AllSubscribed, + + Room(OwnedRoomId), +} + +impl Serialize for RoomReceiptConfig { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + RoomReceiptConfig::AllSubscribed => serializer.serialize_str("*"), + RoomReceiptConfig::Room(r) => r.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for RoomReceiptConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + match deserialize_cow_str(deserializer)?.as_ref() { + "*" => Ok(RoomReceiptConfig::AllSubscribed), + other => Ok(RoomReceiptConfig::Room( + RoomId::parse(other).map_err(D::Error::custom)?.to_owned(), + )), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ReceiptsConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl ReceiptsConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Receipts { + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, +} + +impl Receipts { + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct TypingConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl TypingConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Typing { + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, +} + +impl Typing { + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } +} + +#[cfg(test)] +mod tests { + use ruma_common::owned_room_id; + + use super::RoomReceiptConfig; + + #[test] + fn serialize_room_receipt_config() { + let entry = RoomReceiptConfig::AllSubscribed; + assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#); + + let entry = RoomReceiptConfig::Room(owned_room_id!("!n8f893n9:example.com")); + assert_eq!( + serde_json::to_string(&entry).unwrap().as_str(), + r#""!n8f893n9:example.com""# + ); + } + + #[test] + fn deserialize_room_receipt_config() { + assert_eq!( + serde_json::from_str::(r#""*""#).unwrap(), + RoomReceiptConfig::AllSubscribed + ); + + assert_eq!( + serde_json::from_str::(r#""!n8f893n9:example.com""#).unwrap(), + RoomReceiptConfig::Room(owned_room_id!("!n8f893n9:example.com")) + ); + } +} diff --git a/crates/matrix/src/client-backup/uiaa.rs.bk.bk b/crates/matrix/src/client-backup/uiaa.rs.bk.bk new file mode 100644 index 0000000..c417d30 --- /dev/null +++ b/crates/matrix/src/client-backup/uiaa.rs.bk.bk @@ -0,0 +1,164 @@ +//! Module for [User-Interactive Authentication API][uiaa] types. +//! +//! [uiaa]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api + +use ruma_common::{thirdparty::Medium, OwnedSessionId, OwnedUserId, UserId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UiaaResponse { + pub flows: Vec, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub completed: Vec, + + pub params: Box, + + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, + // #[serde(flatten, skip_serializing_if = "Option::is_none")] + // pub auth_error: Option, +} + +/// Ordered list of stages required to complete authentication. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AuthFlow { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub stages: Vec, +} + +impl AuthFlow { + pub fn new(stages: Vec) -> Self { + Self { stages } + } +} + +/// Information for one authentication stage. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub enum AuthType { + /// Password-based authentication (`m.login.password`). + #[serde(rename = "m.login.password")] + Password, + + /// Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + #[serde(rename = "m.login.recaptcha")] + ReCaptcha, + + /// Email-based authentication (`m.login.email.identity`). + #[serde(rename = "m.login.email.identity")] + EmailIdentity, + + /// Phone number-based authentication (`m.login.msisdn`). + #[serde(rename = "m.login.msisdn")] + Msisdn, + + /// SSO-based authentication (`m.login.sso`). + #[serde(rename = "m.login.sso")] + Sso, + + /// Dummy authentication (`m.login.dummy`). + #[serde(rename = "m.login.dummy")] + Dummy, + + /// Registration token-based authentication (`m.login.registration_token`). + #[serde(rename = "m.login.registration_token")] + RegistrationToken, +} + +#[derive(Clone, Debug, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum AuthData { + // Password-based authentication (`m.login.password`). + Password(Password), + + // Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + // ReCaptcha(ReCaptcha), + + // Email-based authentication (`m.login.email.identity`). + // EmailIdentity(EmailIdentity), + + // Phone number-based authentication (`m.login.msisdn`). + // Msisdn(Msisdn), + + // Dummy authentication (`m.login.dummy`). + Dummy(Dummy), + // Registration token-based authentication (`m.login.registration_token`). + // RegistrationToken(RegistrationToken), + + // Fallback acknowledgement. + // FallbackAcknowledgement(FallbackAcknowledgement), +} + +impl AuthData { + fn kind(&self) -> AuthType { + match self { + AuthData::Password(_) => AuthType::Password, + AuthData::Dummy(_) => AuthType::Dummy, + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.dummy")] +pub struct Dummy {} + +impl Dummy { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.password")] +pub struct Password { + identifier: UserIdentifier, + password: String, +} + +impl Password { + pub fn new>(user_id: impl Into, password: S) -> Self { + let user: &UserId = &user_id.into(); + + Self { + identifier: UserIdentifier::User { + user: user.localpart().to_owned(), + }, + password: password.into(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct UiaaRequest { + session: Option, + + kind: AuthType, + + #[serde(flatten)] + data: AuthData, +} + +impl UiaaRequest { + pub fn new(data: AuthData, session: Option) -> Self { + Self { + session, + kind: data.kind(), + data, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum UserIdentifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} diff --git a/crates/matrix/src/client.rs b/crates/matrix/src/client.rs new file mode 100644 index 0000000..b9f8de2 --- /dev/null +++ b/crates/matrix/src/client.rs @@ -0,0 +1,10 @@ +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +pub mod account; +pub mod login; +pub mod logout; +pub mod profile; +pub mod register; +pub mod uiaa; diff --git a/crates/matrix/src/client/account.rs b/crates/matrix/src/client/account.rs new file mode 100644 index 0000000..7f46308 --- /dev/null +++ b/crates/matrix/src/client/account.rs @@ -0,0 +1,2 @@ +pub mod password; +pub mod whoami; diff --git a/crates/matrix/src/client/account/password.rs b/crates/matrix/src/client/account/password.rs new file mode 100644 index 0000000..7ab5fc0 --- /dev/null +++ b/crates/matrix/src/client/account/password.rs @@ -0,0 +1,55 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; +use serde::Serialize; + +use crate::client::uiaa::{self, Auth, AuthData}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/account/password", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub auth: Auth, + + pub logout_devices: bool, + + pub new_password: String, +} + +impl Request { + pub fn new(new_password: String) -> Self { + Self { + auth: Auth::new(AuthData::Dummy(uiaa::Dummy {}), None), + logout_devices: false, + new_password, + } + } + + pub fn with_password( + mut self, + user_id: OwnedUserId, + password: String, + // auth_session: Option>, + ) -> Self { + self.auth = Auth::new( + AuthData::Password(uiaa::Password::new(user_id, password)), + // auth_session.map(Into::into), + None, + ); + + self + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response {} diff --git a/crates/matrix/src/client/account/whoami.rs b/crates/matrix/src/client/account/whoami.rs new file mode 100644 index 0000000..c2f1f73 --- /dev/null +++ b/crates/matrix/src/client/account/whoami.rs @@ -0,0 +1,32 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/account/whoami", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +impl Request { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self {} + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response { + pub device_id: OwnedDeviceId, + pub user_id: OwnedUserId, +} diff --git a/crates/matrix/src/client/login.rs b/crates/matrix/src/client/login.rs new file mode 100644 index 0000000..153f0d6 --- /dev/null +++ b/crates/matrix/src/client/login.rs @@ -0,0 +1,134 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedMxcUri, OwnedUserId, +}; +use serde::{Deserialize, Serialize}; + +use crate::client::uiaa::UserIdentifier; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/login", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(flatten, rename = "type")] + pub kind: LoginType, + + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + + #[serde( + rename = "initial_device_display_name", + skip_serializing_if = "String::is_empty" + )] + pub device_name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +impl Request { + pub fn new( + kind: LoginType, + identifier: Option, + device_name: String, + refresh_token: Option, + ) -> Self { + Self { + kind, + identifier, + device_name, + refresh_token, + } + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize, Serialize)] +pub struct Response { + pub access_token: String, + + pub device_id: OwnedDeviceId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in_ms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub well_known: Option, +} + +// impl Response { +// pub fn new>( +// access_token: S, +// refresh_token: Option, +// expires_in_ms: Option, +// user_id: impl Into, +// device_id: impl Into, +// well_known: Option, +// ) -> Self { +// Self { +// access_token: access_token.into(), +// refresh_token: refresh_token.map(Into::into), +// expires_in_ms, +// device_id: device_id.into(), +// user_id: user_id.into(), +// well_known, +// } +// } +// } + +#[derive(Clone, Debug, Serialize)] +pub struct IdentityProvider { + pub id: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum LoginType { + #[serde(rename = "m.login.password")] + Password { password: String }, + + #[serde(rename = "m.login.token")] + Token { token: String }, + + #[serde(rename = "m.login.sso")] + Sso { + #[serde(skip_serializing_if = "<[_]>::is_empty")] + identity_providers: Vec, + }, + + #[serde(rename = "m.login.application_service")] + ApplicationService, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BaseUrl { + pub base_url: url::Url, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WellKnown { + #[serde(rename = "m.homeserver")] + pub homeserver: BaseUrl, + + #[serde(rename = "m.identity_server")] + pub identity_server: BaseUrl, +} diff --git a/crates/matrix/src/client/logout.rs b/crates/matrix/src/client/logout.rs new file mode 100644 index 0000000..2422763 --- /dev/null +++ b/crates/matrix/src/client/logout.rs @@ -0,0 +1,2 @@ +pub mod all; +pub mod root; diff --git a/crates/matrix/src/client/logout/all.rs b/crates/matrix/src/client/logout/all.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/matrix/src/client/logout/all.rs @@ -0,0 +1 @@ + diff --git a/crates/matrix/src/client/logout/root.rs b/crates/matrix/src/client/logout/root.rs new file mode 100644 index 0000000..10d0cbb --- /dev/null +++ b/crates/matrix/src/client/logout/root.rs @@ -0,0 +1,29 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/logout", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[allow(clippy::new_without_default)] +impl Request { + pub fn new() -> Self { + Self {} + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize, Serialize)] +pub struct Response {} diff --git a/crates/matrix/src/client/profile.rs b/crates/matrix/src/client/profile.rs new file mode 100644 index 0000000..58428f4 --- /dev/null +++ b/crates/matrix/src/client/profile.rs @@ -0,0 +1,2 @@ +pub mod avatar_url; +pub mod display_name; diff --git a/crates/matrix/src/client/profile/avatar_url.rs b/crates/matrix/src/client/profile/avatar_url.rs new file mode 100644 index 0000000..0e93baa --- /dev/null +++ b/crates/matrix/src/client/profile/avatar_url.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod update; diff --git a/crates/matrix/src/client/profile/avatar_url/get.rs b/crates/matrix/src/client/profile/avatar_url/get.rs new file mode 100644 index 0000000..1d18ad6 --- /dev/null +++ b/crates/matrix/src/client/profile/avatar_url/get.rs @@ -0,0 +1,31 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/avatar_url", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, +} + +impl Request { + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id } + } +} + +#[response(error = crate::Error)] +pub struct Response { + pub avatar_url: OwnedMxcUri, +} diff --git a/crates/matrix/src/client/profile/avatar_url/update.rs b/crates/matrix/src/client/profile/avatar_url/update.rs new file mode 100644 index 0000000..d291379 --- /dev/null +++ b/crates/matrix/src/client/profile/avatar_url/update.rs @@ -0,0 +1,36 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, OwnedUserId, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/avatar_url", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + pub avatar_url: OwnedMxcUri, +} + +impl Request { + pub fn new(user_id: OwnedUserId, avatar_url: OwnedMxcUri) -> Self { + Self { + user_id, + avatar_url, + } + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response {} diff --git a/crates/matrix/src/client/profile/display_name.rs b/crates/matrix/src/client/profile/display_name.rs new file mode 100644 index 0000000..0e93baa --- /dev/null +++ b/crates/matrix/src/client/profile/display_name.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod update; diff --git a/crates/matrix/src/client/profile/display_name/get.rs b/crates/matrix/src/client/profile/display_name/get.rs new file mode 100644 index 0000000..7ce9d9a --- /dev/null +++ b/crates/matrix/src/client/profile/display_name/get.rs @@ -0,0 +1,32 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/displayname", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, +} + +impl Request { + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id } + } +} + +#[response(error = crate::Error)] +pub struct Response { + #[serde(rename = "displayname")] + pub display_name: String, +} diff --git a/crates/matrix/src/client/profile/display_name/update.rs b/crates/matrix/src/client/profile/display_name/update.rs new file mode 100644 index 0000000..5cac54f --- /dev/null +++ b/crates/matrix/src/client/profile/display_name/update.rs @@ -0,0 +1,35 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/displayname", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[serde(rename = "displayname")] + pub display_name: String, +} + +impl Request { + pub fn new(user_id: OwnedUserId, display_name: String) -> Self { + Self { + user_id, + display_name, + } + } +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/register.rs b/crates/matrix/src/client/register.rs new file mode 100644 index 0000000..b518083 --- /dev/null +++ b/crates/matrix/src/client/register.rs @@ -0,0 +1,3 @@ +pub mod available; +pub mod root; +pub mod token; diff --git a/crates/matrix/src/client/register/available.rs b/crates/matrix/src/client/register/available.rs new file mode 100644 index 0000000..f5ecc37 --- /dev/null +++ b/crates/matrix/src/client/register/available.rs @@ -0,0 +1,33 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/register/available", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + pub username: String, +} + +impl Request { + pub fn new(username: String) -> Self { + Self { username } + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response { + pub available: bool, +} diff --git a/crates/matrix/src/client/register/root.rs b/crates/matrix/src/client/register/root.rs new file mode 100644 index 0000000..860021d --- /dev/null +++ b/crates/matrix/src/client/register/root.rs @@ -0,0 +1,76 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; +use serde::{Deserialize, Serialize}; + +use crate::client::uiaa::Auth; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub username: String, + + pub password: String, + + #[serde( + rename = "initial_device_display_name", + skip_serializing_if = "Option::is_none" + )] + pub device_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + /// Note that this information is not used to define how the registered user + /// should be authenticated, but is instead used to authenticate the + /// register call itself. It should be left empty, or omitted, unless an + /// earlier call returned an response with status code 401. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +impl Request { + pub fn new( + username: String, + password: String, + device_name: Option, + refresh_token: Option, + auth: Option, + ) -> Self { + Self { + username, + password, + device_name, + refresh_token, + auth, + } + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize, Serialize)] +pub struct Response { + #[serde(default)] + pub access_token: Option, + + #[serde(default)] + pub device_id: Option, + + #[serde(default)] + pub expires_in_ms: Option, + + #[serde(default)] + pub refresh_token: Option, + + pub user_id: OwnedUserId, +} diff --git a/crates/matrix/src/client/register/token.rs b/crates/matrix/src/client/register/token.rs new file mode 100644 index 0000000..113a424 --- /dev/null +++ b/crates/matrix/src/client/register/token.rs @@ -0,0 +1 @@ +pub mod validity; diff --git a/crates/matrix/src/client/register/token/validity.rs b/crates/matrix/src/client/register/token/validity.rs new file mode 100644 index 0000000..98c9fa9 --- /dev/null +++ b/crates/matrix/src/client/register/token/validity.rs @@ -0,0 +1,31 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v1/register/m.login.registration_token/validity", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + pub token: String, +} + +impl Request { + pub fn new(token: String) -> Self { + Self { token } + } +} + +#[response(error = crate::Error)] +pub struct Response { + pub valid: bool, +} diff --git a/crates/matrix/src/client/uiaa.rs b/crates/matrix/src/client/uiaa.rs new file mode 100644 index 0000000..a49a517 --- /dev/null +++ b/crates/matrix/src/client/uiaa.rs @@ -0,0 +1,164 @@ +//! Module for [User-Interactive Authentication API][uiaa] types. +//! +//! [uiaa]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api + +use ruma_common::{thirdparty::Medium, OwnedSessionId, OwnedUserId, UserId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UiaaResponse { + pub flows: Vec, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub completed: Vec, + + pub params: Box, + + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, + // #[serde(flatten, skip_serializing_if = "Option::is_none")] + // pub auth_error: Option, +} + +/// Ordered list of stages required to complete authentication. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AuthFlow { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub stages: Vec, +} + +impl AuthFlow { + pub fn new(stages: Vec) -> Self { + Self { stages } + } +} + +/// Information for one authentication stage. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub enum AuthType { + /// Password-based authentication (`m.login.password`). + #[serde(rename = "m.login.password")] + Password, + + /// Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + #[serde(rename = "m.login.recaptcha")] + ReCaptcha, + + /// Email-based authentication (`m.login.email.identity`). + #[serde(rename = "m.login.email.identity")] + EmailIdentity, + + /// Phone number-based authentication (`m.login.msisdn`). + #[serde(rename = "m.login.msisdn")] + Msisdn, + + /// SSO-based authentication (`m.login.sso`). + #[serde(rename = "m.login.sso")] + Sso, + + /// Dummy authentication (`m.login.dummy`). + #[serde(rename = "m.login.dummy")] + Dummy, + + /// Registration token-based authentication (`m.login.registration_token`). + #[serde(rename = "m.login.registration_token")] + RegistrationToken, +} + +#[derive(Clone, Debug, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum AuthData { + // Password-based authentication (`m.login.password`). + Password(Password), + + // Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + // ReCaptcha(ReCaptcha), + + // Email-based authentication (`m.login.email.identity`). + // EmailIdentity(EmailIdentity), + + // Phone number-based authentication (`m.login.msisdn`). + // Msisdn(Msisdn), + + // Dummy authentication (`m.login.dummy`). + Dummy(Dummy), + // Registration token-based authentication (`m.login.registration_token`). + // RegistrationToken(RegistrationToken), + + // Fallback acknowledgement. + // FallbackAcknowledgement(FallbackAcknowledgement), +} + +impl AuthData { + fn kind(&self) -> AuthType { + match self { + AuthData::Password(_) => AuthType::Password, + AuthData::Dummy(_) => AuthType::Dummy, + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.dummy")] +pub struct Dummy {} + +impl Dummy { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.password")] +pub struct Password { + identifier: UserIdentifier, + password: String, +} + +impl Password { + pub fn new>(user_id: impl Into, password: S) -> Self { + let user: &UserId = &user_id.into(); + + Self { + identifier: UserIdentifier::User { + user: user.localpart().to_owned(), + }, + password: password.into(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct Auth { + session: Option, + + kind: AuthType, + + #[serde(flatten)] + data: AuthData, +} + +impl Auth { + pub fn new(data: AuthData, session: Option) -> Self { + Self { + session, + kind: data.kind(), + data, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum UserIdentifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs new file mode 100644 index 0000000..6a7e5be --- /dev/null +++ b/crates/matrix/src/lib.rs @@ -0,0 +1,58 @@ +//! This library deals with forwarding Matrix requests to the server. +//! Comments have been used sparingly as the specification contains all the +//! technical details. + +//! We rely on `ruma` to abstract away the boilerplate introduced by HTTP +//! requests, without sacrificing flexibility by defining our own request and +//! response types. +//! +//! reference: https://docs.ruma.io/ruma_common/api/index.html + +pub mod admin; +pub mod client; + +use async_trait::async_trait; +use bytes::{Bytes, BytesMut}; +use ruma_client::HttpClient; + +pub use ruma_client; +pub use ruma_common; +pub use ruma_events; +pub use ruma_identifiers_validation; + +pub type Error = ruma_common::api::error::MatrixError; +pub type HandleError = ruma_client::Error; + +#[derive(Default, Debug)] +pub struct Client { + inner: reqwest::Client, +} + +#[async_trait] +impl HttpClient for Client { + type RequestBody = BytesMut; + type ResponseBody = Bytes; + type Error = reqwest::Error; + + async fn send_http_request( + &self, + req: http::Request, + ) -> Result, reqwest::Error> { + let req = req.map(|body| body.freeze()).try_into()?; + let mut res = self.inner.execute(req).await?; + + let mut http_builder = http::Response::builder() + .status(res.status()) + .version(res.version()); + std::mem::swap( + http_builder + .headers_mut() + .expect("http::response::Builder to be usable"), + res.headers_mut(), + ); + + Ok(http_builder + .body(res.bytes().await?) + .expect("http::Response construction to work")) + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml new file mode 100644 index 0000000..c44e461 --- /dev/null +++ b/crates/router/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "router" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "commune-server" +path = "src/main.rs" + +[lib] +name = "router" +path = "src/lib.rs" + +[dependencies] +axum = { workspace = true, features = ["tokio", "macros"] } +axum-extra = { workspace = true, features = ["typed-header"] } +anyhow = { workspace = true } +http = { workspace = true } +email_address = { workspace = true } +# openssl = { workspace = true, features = ["vendored"] } +# openssl-sys = { workspace = true, features = ["vendored"] } +serde = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +url = { workspace = true, features = ["serde"] } + +# Local Dependencies +core = { path = "../core" } +matrix = { path = "../matrix" } +figment = { workspace = true, features = ["toml", "env"] } diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs new file mode 100644 index 0000000..9756a11 --- /dev/null +++ b/crates/router/src/api.rs @@ -0,0 +1,7 @@ +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +pub mod account; +pub mod relative; +// pub mod session; diff --git a/crates/router/src/api/account.rs b/crates/router/src/api/account.rs new file mode 100644 index 0000000..080944c --- /dev/null +++ b/crates/router/src/api/account.rs @@ -0,0 +1,5 @@ +pub mod avatar; +pub mod display_name; +pub mod email; +pub mod password; +pub mod whoami; diff --git a/crates/router/src/api/account/avatar.rs b/crates/router/src/api/account/avatar.rs new file mode 100644 index 0000000..2dbb812 --- /dev/null +++ b/crates/router/src/api/account/avatar.rs @@ -0,0 +1,31 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use matrix::ruma_common::OwnedMxcUri; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub mxc_uri: OwnedMxcUri, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service(access_token.token(), payload.mxc_uri).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update avatar"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/display_name.rs b/crates/router/src/api/account/display_name.rs new file mode 100644 index 0000000..7467469 --- /dev/null +++ b/crates/router/src/api/account/display_name.rs @@ -0,0 +1,30 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub display_name: String, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service(access_token.token(), payload.display_name).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update display name"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/email.rs b/crates/router/src/api/account/email.rs new file mode 100644 index 0000000..6fb66d1 --- /dev/null +++ b/crates/router/src/api/account/email.rs @@ -0,0 +1,19 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; +use email_address::EmailAddress; + +pub async fn handler(Path(email): Path) -> Response { + use commune::account::email::service; + + match service(email).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to handle email verification"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/password.rs b/crates/router/src/api/account/password.rs new file mode 100644 index 0000000..37d28e4 --- /dev/null +++ b/crates/router/src/api/account/password.rs @@ -0,0 +1,40 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + username: String, + password: Secret, + new_password: Secret, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::account::password::service; + + match service( + access_token.token(), + payload.username, + payload.password, + payload.new_password, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to reset password"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/whoami.rs b/crates/router/src/api/account/whoami.rs new file mode 100644 index 0000000..c5a1f91 --- /dev/null +++ b/crates/router/src/api/account/whoami.rs @@ -0,0 +1,21 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; + +pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { + use commune::account::whoami::service; + + match service(access_token.token()).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to associate access token with user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative.rs b/crates/router/src/api/relative.rs new file mode 100644 index 0000000..a5a6a98 --- /dev/null +++ b/crates/router/src/api/relative.rs @@ -0,0 +1,4 @@ +pub mod available; +pub mod login; +pub mod logout; +pub mod register; diff --git a/crates/router/src/api/relative/available.rs b/crates/router/src/api/relative/available.rs new file mode 100644 index 0000000..e37cbb0 --- /dev/null +++ b/crates/router/src/api/relative/available.rs @@ -0,0 +1,18 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn handler(Path(username): Path) -> Response { + use commune::account::username::service; + + match service(username).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to check username availability"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/login.rs b/crates/router/src/api/relative/login.rs new file mode 100644 index 0000000..d8501f8 --- /dev/null +++ b/crates/router/src/api/relative/login.rs @@ -0,0 +1,25 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Payload { + pub username: String, + pub password: Secret, +} + +pub async fn handler(Json(payload): Json) -> Response { + use commune::account::login::service; + + match service(&payload.username, &payload.password).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to login user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/logout.rs b/crates/router/src/api/relative/logout.rs new file mode 100644 index 0000000..2a492da --- /dev/null +++ b/crates/router/src/api/relative/logout.rs @@ -0,0 +1,21 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; + +pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { + use commune::account::logout::service; + + match service(access_token.token()).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to logout user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/register.rs b/crates/router/src/api/relative/register.rs new file mode 100644 index 0000000..e9a5323 --- /dev/null +++ b/crates/router/src/api/relative/register.rs @@ -0,0 +1,25 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + pub username: String, + pub password: Secret, +} + +pub async fn handler(Json(payload): Json) -> Response { + use commune::account::register::service; + + match service(payload.username, payload.password).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to create account"); + + e.into_response() + } + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs new file mode 100644 index 0000000..a3785e1 --- /dev/null +++ b/crates/router/src/lib.rs @@ -0,0 +1,48 @@ +use std::net::SocketAddr; + +use axum::{ + routing::{get, post, put}, + Router, +}; +use tokio::net::TcpListener; + +pub mod api; + +pub async fn routes() -> Router { + let router = Router::new() + .route("/register", post(api::relative::register::handler)) + .route( + "/register/available/:username", + get(api::relative::available::handler), + ) + .route("/login", post(api::relative::login::handler)) + .route("/logout", post(api::relative::logout::handler)) + .nest( + "/account", + Router::new() + .route("/whoami", get(api::account::whoami::handler)) + .route("/password", put(api::account::password::handler)) + .route("/display_name", put(api::account::display_name::handler)) + .route("/avatar", put(api::account::avatar::handler)), + ); + + Router::new().nest("/_commune/client/r0", router) +} + +pub async fn serve(public_loopback: bool, port: u16) -> anyhow::Result<()> { + let host = match public_loopback { + true => [0, 0, 0, 0], + false => [127, 0, 0, 1], + }; + + let addr = SocketAddr::from((host, port)); + let tcp_listener = TcpListener::bind(addr).await?; + + tracing::info!("Listening on {}", addr); + + let router = routes().await; + + axum::serve(tcp_listener, router.into_make_service()) + .await + .map_err(Into::into) +} diff --git a/crates/router/src/main.rs b/crates/router/src/main.rs new file mode 100644 index 0000000..7991edc --- /dev/null +++ b/crates/router/src/main.rs @@ -0,0 +1,13 @@ +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + commune::init().await; + let config = &commune::commune().config; + + router::serve(config.public_loopback, config.port.unwrap()).await?; + + Ok(()) +} diff --git a/crates/router/src/router/api/mod.rs b/crates/router/src/router/api/mod.rs new file mode 100644 index 0000000..bd8c77a --- /dev/null +++ b/crates/router/src/router/api/mod.rs @@ -0,0 +1,90 @@ +pub mod v1; + +use axum::{response::IntoResponse, Json, Router}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; + +use commune::error::HttpStatusCode; + +pub struct Api; + +impl Api { + pub fn routes() -> Router { + Router::new().nest("/api", Router::new().nest("/v1", v1::V1::routes())) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ApiError { + pub message: String, + pub code: String, + #[serde(skip)] + pub status: StatusCode, +} + +impl ApiError { + pub fn new(message: String, code: String, status: StatusCode) -> Self { + Self { + message, + code, + status, + } + } + + pub fn unauthorized() -> Self { + Self::new( + "You must be authenticated to access this resource".to_string(), + "UNAUTHORIZED".to_string(), + StatusCode::UNAUTHORIZED, + ) + } + + pub fn internal_server_error() -> Self { + Self::new( + "Internal server error".to_string(), + "INTERNAL_SERVER_ERROR".to_string(), + StatusCode::INTERNAL_SERVER_ERROR, + ) + } +} + +impl From for ApiError { + fn from(err: commune::error::Error) -> Self { + Self { + message: err.to_string(), + code: err.error_code().to_string(), + status: err.status_code(), + } + } +} + +/// Any `anyhow::Error` can be converted into an `ApiError`. +/// +/// Caveat is that given that anyhow error is generic (w/o context), the +/// error status is 500. +/// +/// Perhaps in the future, a more specific error type can be used, like with +/// `thiserror`. +impl From for ApiError { + fn from(err: anyhow::Error) -> Self { + Self { + message: err.to_string(), + code: "UNKNOWN_ERROR".to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + if let Ok(status) = axum::http::StatusCode::from_u16(self.status.as_u16()) { + let mut response = Json(self).into_response(); + + *response.status_mut() = status; + return response; + } + + tracing::error!(status=%self.status, "Failed to convert status code to http::StatusCode"); + ApiError::internal_server_error().into_response() + } +} diff --git a/crates/router/src/router/api/v1/account/email.rs b/crates/router/src/router/api/v1/account/email.rs new file mode 100644 index 0000000..004d93a --- /dev/null +++ b/crates/router/src/router/api/v1/account/email.rs @@ -0,0 +1,34 @@ +use axum::{ + extract::Path, + http::StatusCode, + response::{IntoResponse, Response}, + Extension, Json, +}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use crate::{router::api::ApiError, services::SharedServices}; + +#[instrument(skip(services))] +pub async fn handler( + Extension(services): Extension, + Path(email): Path, +) -> Response { + match services.commune.account.is_email_available(&email).await { + Ok(available) => { + let mut response = Json(AccountEmailExistsResponse { available }).into_response(); + + *response.status_mut() = StatusCode::OK; + response + } + Err(err) => { + tracing::warn!(?err, ?email, "Failed to find email"); + ApiError::from(err).into_response() + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AccountEmailExistsResponse { + pub available: bool, +} diff --git a/crates/router/src/router/api/v1/account/login.rs b/crates/router/src/router/api/v1/account/login.rs new file mode 100644 index 0000000..0257bcd --- /dev/null +++ b/crates/router/src/router/api/v1/account/login.rs @@ -0,0 +1,95 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Extension, Json, +}; +use commune::Error; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use commune::auth::service::LoginCredentials; + +use crate::{router::api::ApiError, services::SharedServices}; + +use super::root::{AccountMatrixCredentials, AccountSpace}; + +#[instrument(skip(services))] +pub async fn get(Extension(services): Extension) -> Response { + match services.commune.auth.get_login_flows().await { + Ok(flows) => Json(flows).into_response(), + Err(err) => { + tracing::warn!(?err, "Failed to retrieve login flows"); + ApiError::from(err).into_response() + } + } +} + +#[instrument(skip(services, payload))] +pub async fn post( + Extension(services): Extension, + Json(payload): Json, +) -> Response { + let login_credentials = LoginCredentials::from(payload); + + let Ok(tokens) = services.commune.auth.login(login_credentials).await else { + tracing::warn!("Failed to authenticate user"); + return ApiError::from(Error::Auth( + commune::auth::error::AuthErrorCode::InvalidCredentials, + )) + .into_response(); + }; + + match services.commune.account.whoami(&tokens.access_token).await { + Ok(account) => { + let mut response = Json(AccountLoginResponse { + access_token: tokens.access_token.to_string(), + credentials: AccountMatrixCredentials { + username: account.username, + display_name: account.display_name, + avatar_url: account.avatar_url, + access_token: tokens.access_token.to_string(), + matrix_access_token: tokens.access_token.to_string(), + matrix_user_id: account.user_id.to_string(), + matrix_device_id: String::new(), + user_space_id: String::new(), + email: account.email, + age: account.age, + admin: account.admin, + verified: account.verified, + }, + ..Default::default() + }) + .into_response(); + + *response.status_mut() = StatusCode::OK; + response + } + Err(err) => { + tracing::warn!(?err, "Failed to authenticate user"); + ApiError::from(err).into_response() + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AccountLoginPayload { + pub username: String, + pub password: String, +} + +impl From for LoginCredentials { + fn from(payload: AccountLoginPayload) -> Self { + Self { + username: payload.username, + password: payload.password.into(), + } + } +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AccountLoginResponse { + pub access_token: String, + pub credentials: AccountMatrixCredentials, + pub rooms: Vec, + pub spaces: Vec, +} diff --git a/crates/router/src/router/api/v1/account/mod.rs b/crates/router/src/router/api/v1/account/mod.rs new file mode 100644 index 0000000..a1b74f6 --- /dev/null +++ b/crates/router/src/router/api/v1/account/mod.rs @@ -0,0 +1,35 @@ +pub mod email; +pub mod login; +pub mod root; +pub mod session; +pub mod verify_code; +pub mod verify_code_email; + +use axum::{ + middleware, + routing::{get, post}, + Router, +}; + +use crate::router::middleware::auth; + +pub struct Account; + +impl Account { + pub fn routes() -> Router { + Router::new() + .route("/session", get(session::handler)) + .route_layer(middleware::from_fn(auth)) + .route("/", post(root::handler)) + .route("/login", get(login::get)) + .route("/login", post(login::post)) + .route("/login/sso/redirect", get(login::get)) + .route("/email/:email", get(email::handler)) + .nest( + "/verify", + Router::new() + .route("/code", post(verify_code::handler)) + .route("/code/email", post(verify_code_email::handler)), + ) + } +} diff --git a/crates/router/src/router/api/v1/account/root.rs b/crates/router/src/router/api/v1/account/root.rs new file mode 100644 index 0000000..6358ded --- /dev/null +++ b/crates/router/src/router/api/v1/account/root.rs @@ -0,0 +1,128 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Extension, Json, +}; + +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use commune::account::{model::Account, service::CreateAccountDto}; +use url::Url; +use uuid::Uuid; + +use crate::{router::api::ApiError, services::SharedServices}; + +#[instrument(skip(services, payload))] +pub async fn handler( + Extension(services): Extension, + Json(payload): Json, +) -> Response { + let dto = CreateAccountDto::from(payload); + + match services.commune.account.register(dto).await { + Ok(account) => { + let access_token = services + .commune + .account + .issue_user_token(&account.user_id) + .await + .unwrap(); + let payload = AccountRegisterResponse { + access_token: access_token.to_string(), + created: true, + credentials: AccountMatrixCredentials { + username: account.username, + display_name: account.display_name, + avatar_url: account.avatar_url, + access_token: access_token.to_string(), + matrix_access_token: access_token.to_string(), + matrix_user_id: account.user_id.to_string(), + matrix_device_id: "".to_string(), + user_space_id: "".to_string(), + email: account.email, + age: account.age, + admin: account.admin, + verified: account.verified, + }, + ..Default::default() + }; + + let mut response = Json(payload).into_response(); + + *response.status_mut() = StatusCode::CREATED; + response + } + Err(err) => { + tracing::warn!(?err, "Failed to register user"); + ApiError::from(err).into_response() + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AccountRegisterPayload { + pub username: String, + pub password: String, + pub email: String, + pub session: Uuid, + pub code: String, +} + +impl From for CreateAccountDto { + fn from(payload: AccountRegisterPayload) -> Self { + Self { + username: payload.username, + password: payload.password.into(), + email: payload.email, + session: payload.session, + code: payload.code.into(), + } + } +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AccountSpace { + pub room_id: String, + pub alias: String, + pub name: String, + pub topic: Option, + pub avatar: Option, + pub header: Option, + pub is_profile: bool, + pub is_default: bool, + pub is_owner: bool, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AccountMatrixCredentials { + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub access_token: String, + pub matrix_access_token: String, + pub matrix_user_id: String, + pub matrix_device_id: String, + pub user_space_id: String, + pub email: String, + pub age: i64, + pub admin: bool, + pub verified: bool, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AccountRegisterResponse { + pub access_token: String, + pub created: bool, + pub credentials: AccountMatrixCredentials, + pub rooms: Vec, + pub spaces: Vec, +} + +impl From for AccountRegisterResponse { + fn from(_: Account) -> Self { + Self { + ..Default::default() + } + } +} diff --git a/crates/router/src/router/api/v1/account/session.rs b/crates/router/src/router/api/v1/account/session.rs new file mode 100644 index 0000000..f17c737 --- /dev/null +++ b/crates/router/src/router/api/v1/account/session.rs @@ -0,0 +1,48 @@ +use axum::{ + response::{IntoResponse, Response}, + Extension, Json, +}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use commune::account::model::Account; + +use crate::router::middleware::AccessToken; + +use super::root::{AccountMatrixCredentials, AccountSpace}; + +#[instrument(skip(account))] +pub async fn handler( + Extension(account): Extension, + Extension(access_token): Extension, +) -> Response { + let response = Json(AccountSessionResponse { + credentials: AccountMatrixCredentials { + username: account.username, + display_name: account.display_name, + avatar_url: account.avatar_url, + access_token: access_token.to_string(), + matrix_access_token: access_token.to_string(), + matrix_user_id: account.user_id.to_string(), + matrix_device_id: String::new(), + user_space_id: String::new(), + email: account.email, + age: account.age, + admin: account.admin, + verified: account.verified, + }, + rooms: vec![], + spaces: vec![], + valid: true, + }); + + response.into_response() +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AccountSessionResponse { + pub credentials: AccountMatrixCredentials, + pub rooms: Vec, + pub spaces: Vec, + pub valid: bool, +} diff --git a/crates/router/src/router/api/v1/account/verify_code.rs b/crates/router/src/router/api/v1/account/verify_code.rs new file mode 100644 index 0000000..0f135e0 --- /dev/null +++ b/crates/router/src/router/api/v1/account/verify_code.rs @@ -0,0 +1,74 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Extension, Json, +}; +use commune::{account::error::AccountErrorCode, Error}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use uuid::Uuid; + +use commune::account::service::SendCodeDto; + +use crate::{router::api::ApiError, services::SharedServices}; + +#[instrument(skip(services, payload))] +pub async fn handler( + Extension(services): Extension, + Json(payload): Json, +) -> Response { + let dto = SendCodeDto::from(payload); + + match services + .commune + .account + .is_email_available(&dto.email) + .await + { + Ok(available) => { + if !available { + let email_taken_error = AccountErrorCode::EmailTaken(dto.email); + let error = Error::User(email_taken_error); + + return ApiError::from(error).into_response(); + } + } + Err(err) => { + tracing::warn!(?err, ?dto, "Failed to verify email availability"); + return ApiError::from(err).into_response(); + } + } + + match services.commune.account.send_code(dto).await { + Ok(_) => { + let mut response = Json(VerifyCodeResponse { sent: true }).into_response(); + + *response.status_mut() = StatusCode::OK; + response + } + Err(err) => { + tracing::warn!(?err, "Failed to register user"); + ApiError::from(err).into_response() + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AccountVerifyCodePayload { + pub email: String, + pub session: Uuid, +} + +impl From for SendCodeDto { + fn from(payload: AccountVerifyCodePayload) -> Self { + Self { + email: payload.email, + session: payload.session, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct VerifyCodeResponse { + pub sent: bool, +} diff --git a/crates/router/src/router/api/v1/account/verify_code_email.rs b/crates/router/src/router/api/v1/account/verify_code_email.rs new file mode 100644 index 0000000..a5b8205 --- /dev/null +++ b/crates/router/src/router/api/v1/account/verify_code_email.rs @@ -0,0 +1,76 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Extension, Json, +}; +use commune::{account::error::AccountErrorCode, util::secret::Secret, Error}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use uuid::Uuid; + +use commune::account::service::VerifyCodeDto; + +use crate::{router::api::ApiError, services::SharedServices}; + +#[instrument(skip(services, payload))] +pub async fn handler( + Extension(services): Extension, + Json(payload): Json, +) -> Response { + let dto = VerifyCodeDto::from(payload); + + match services + .commune + .account + .is_email_available(&dto.email) + .await + { + Ok(available) => { + if !available { + let email_taken_error = AccountErrorCode::EmailTaken(dto.email); + let error = Error::User(email_taken_error); + + return ApiError::from(error).into_response(); + } + } + Err(err) => { + tracing::warn!(?err, ?dto, "Failed to verify email availability"); + return ApiError::from(err).into_response(); + } + } + + match services.commune.account.verify_code(dto).await { + Ok(valid) => { + let mut response = Json(VerifyCodeEmailResponse { valid }).into_response(); + + *response.status_mut() = StatusCode::OK; + response + } + Err(err) => { + tracing::warn!(?err, "Failed to register user"); + ApiError::from(err).into_response() + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AccountVerifyCodeEmailPayload { + pub email: String, + pub session: Uuid, + pub code: Secret, +} + +impl From for VerifyCodeDto { + fn from(payload: AccountVerifyCodeEmailPayload) -> Self { + Self { + email: payload.email, + session: payload.session, + code: payload.code, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct VerifyCodeEmailResponse { + pub valid: bool, +} diff --git a/crates/router/src/router/api/v1/mod.rs b/crates/router/src/router/api/v1/mod.rs new file mode 100644 index 0000000..360418c --- /dev/null +++ b/crates/router/src/router/api/v1/mod.rs @@ -0,0 +1,11 @@ +pub mod account; + +use axum::Router; + +pub struct V1; + +impl V1 { + pub fn routes() -> Router { + Router::new().nest("/account", account::Account::routes()) + } +} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml new file mode 100644 index 0000000..382d607 --- /dev/null +++ b/crates/test/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "test" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +name = "test" +path = "src/lib.rs" + +[dependencies] +# Workspace Dependencies +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio"] } +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true } +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +thiserror = { workspace = true } +url = { workspace = true } +rand = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +# Local Dependencies +core = { path = "../core" } +matrix = { path = "../matrix" } +router = { path = "../router" } diff --git a/crates/test/fixtures/synapse/homeserver.yaml b/crates/test/fixtures/synapse/homeserver.yaml new file mode 100644 index 0000000..f6450c8 --- /dev/null +++ b/crates/test/fixtures/synapse/homeserver.yaml @@ -0,0 +1,84 @@ +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html +# +# For more information on how to configure Synapse, including a complete accounting of +# each option, go to docs/usage/configuration/config_documentation.md or +# https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html +server_name: "matrix.localhost" +pid_file: /data/homeserver.pid + +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + bind_addresses: ['::', '0.0.0.0'] + resources: + - names: [client, federation] + compress: false +database: + name: psycopg2 + txn_limit: 10000 + allow_unsafe_locale: true + args: + user: synapse_user + password: secretpassword + database: synapse + host: localhost + port: 5432 + cp_min: 5 + cp_max: 10 +log_config: "/data/matrix.localhost.log.config" +media_store_path: /data/media_store +registration_shared_secret: "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B" +report_stats: true +macaroon_secret_key: "XND.g+P_7wz.Yx:i6js.Eh;=jG*#uWBIe;X2OoX78^E,LVJ;8c" +form_secret: "pS7pR@AFJD~BtUAqH^ku5Kenz1X^Hol0E_+xhwvohOrkx;sMoO" +signing_key_path: "/data/matrix.localhost.signing.key" +trusted_key_servers: + - server_name: "matrix.org" + +rc_message: + per_second: 1000 + burst_count: 1000 +rc_registration: + per_second: 1000 + burst_count: 1000 +rc_login: + address: + per_second: 1000 + burst_count: 1000 + account: + per_second: 1000 + burst_count: 1000 + failed_attempts: + per_second: 1000 + burst_count: 1000 +rc_admin_redaction: + per_second: 1000 + burst_count: 1000 +rc_joins: + local: + per_second: 1000 + burst_count: 1000 + remote: + per_second: 1000 + burst_count: 1000 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +enable_registration: true +enable_registration_without_verification: true diff --git a/crates/test/fixtures/synapse/matrix.localhost.log.config b/crates/test/fixtures/synapse/matrix.localhost.log.config new file mode 100644 index 0000000..1fda721 --- /dev/null +++ b/crates/test/fixtures/synapse/matrix.localhost.log.config @@ -0,0 +1,35 @@ +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + file: + class: logging.handlers.TimedRotatingFileHandler + formatter: precise + filename: /data/homeserver.log + when: midnight + backupCount: 3 + encoding: utf8 + + buffer: + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler + target: file + capacity: 10 + + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO +root: + level: INFO + handlers: [console] + + +disable_existing_loggers: false diff --git a/crates/test/fixtures/synapse/matrix.localhost.signing.key b/crates/test/fixtures/synapse/matrix.localhost.signing.key new file mode 100644 index 0000000..090b449 --- /dev/null +++ b/crates/test/fixtures/synapse/matrix.localhost.signing.key @@ -0,0 +1 @@ +ed25519 a_VKUD FXu3HoEKJdiMh1e+3dW8kO/P8ldSdNzdV+/vg9wdowE diff --git a/crates/test/src/api.rs b/crates/test/src/api.rs new file mode 100644 index 0000000..4230649 --- /dev/null +++ b/crates/test/src/api.rs @@ -0,0 +1,7 @@ +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +// pub mod account; +pub mod relative; +// pub mod session; diff --git a/crates/test/src/api/account.rs b/crates/test/src/api/account.rs new file mode 100644 index 0000000..080944c --- /dev/null +++ b/crates/test/src/api/account.rs @@ -0,0 +1,5 @@ +pub mod avatar; +pub mod display_name; +pub mod email; +pub mod password; +pub mod whoami; diff --git a/crates/test/src/api/account/avatar.rs b/crates/test/src/api/account/avatar.rs new file mode 100644 index 0000000..3c55ff8 --- /dev/null +++ b/crates/test/src/api/account/avatar.rs @@ -0,0 +1,33 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use matrix::ruma_common::OwnedMxcUri; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub mxc_uri: OwnedMxcUri, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service( + access_token.token(), + payload.mxc_uri, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update avatar"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/display_name.rs b/crates/test/src/api/account/display_name.rs new file mode 100644 index 0000000..0f9b321 --- /dev/null +++ b/crates/test/src/api/account/display_name.rs @@ -0,0 +1,27 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub display_name: String, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service(access_token.token(), payload.display_name).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update display name"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs new file mode 100644 index 0000000..6fb66d1 --- /dev/null +++ b/crates/test/src/api/account/email.rs @@ -0,0 +1,19 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; +use email_address::EmailAddress; + +pub async fn handler(Path(email): Path) -> Response { + use commune::account::email::service; + + match service(email).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to handle email verification"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/password.rs b/crates/test/src/api/account/password.rs new file mode 100644 index 0000000..b3ea639 --- /dev/null +++ b/crates/test/src/api/account/password.rs @@ -0,0 +1,37 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + username: String, + password: Secret, + new_password: Secret, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::account::password::service; + + match service( + access_token.token(), + payload.username, + payload.password, + payload.new_password, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to reset password"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/whoami.rs b/crates/test/src/api/account/whoami.rs new file mode 100644 index 0000000..c5a1f91 --- /dev/null +++ b/crates/test/src/api/account/whoami.rs @@ -0,0 +1,21 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; + +pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { + use commune::account::whoami::service; + + match service(access_token.token()).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to associate access token with user"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/relative.rs b/crates/test/src/api/relative.rs new file mode 100644 index 0000000..a5a6a98 --- /dev/null +++ b/crates/test/src/api/relative.rs @@ -0,0 +1,4 @@ +pub mod available; +pub mod login; +pub mod logout; +pub mod register; diff --git a/crates/test/src/api/relative/available.rs b/crates/test/src/api/relative/available.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/test/src/api/relative/available.rs @@ -0,0 +1 @@ + diff --git a/crates/test/src/api/relative/login.rs b/crates/test/src/api/relative/login.rs new file mode 100644 index 0000000..cdd28de --- /dev/null +++ b/crates/test/src/api/relative/login.rs @@ -0,0 +1,34 @@ +use commune::util::secret::Secret; +use matrix::client::login::*; +use router::api::relative::login; + +use crate::{api::relative::register, env::Env}; + +pub async fn login(client: &Env) -> Result { + let register_resp = register::register(&client).await.unwrap(); + + tracing::info!(?register_resp); + + let resp = client + .post("/_commune/client/r0/login") + .json(&login::Payload { + username: register_resp.user_id.into(), + password: Secret::new("verysecure"), + }) + .send() + .await + .unwrap(); + + resp.json::().await +} + +#[tokio::test] +async fn login_test() { + let client = Env::new().await; + + let resp = login(&client).await.unwrap(); + + tracing::info!(?resp); + + assert!(!resp.access_token.is_empty()); +} diff --git a/crates/test/src/api/relative/logout.rs b/crates/test/src/api/relative/logout.rs new file mode 100644 index 0000000..eaf583a --- /dev/null +++ b/crates/test/src/api/relative/logout.rs @@ -0,0 +1,30 @@ +use matrix::client::logout::root::*; + +use crate::{api::relative::login, env::Env}; + +pub async fn logout(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .post("/_commune/client/r0/logout") + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await + .unwrap(); + + resp.json::().await +} + +#[tokio::test] +async fn logout_test() { + let client = Env::new().await; + + let resp = logout(&client).await.unwrap(); + + tracing::info!(?resp); +} diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs new file mode 100644 index 0000000..fbe5b88 --- /dev/null +++ b/crates/test/src/api/relative/register.rs @@ -0,0 +1,42 @@ +use commune::util::secret::Secret; +use rand::seq::IteratorRandom; + +use matrix::client::register::root::*; +use router::api::relative::register; + +use crate::env::Env; + +pub async fn register(client: &Env) -> Result { + let allowed = ('0'..='9') + .chain('a'..='z') + .chain(['-', '.', '=', '_', '/', '+']); + let username = allowed + .choose_multiple(&mut rand::thread_rng(), 8) + .into_iter() + .collect(); + + tracing::info!(?username); + + let resp = client + .post("/_commune/client/r0/register") + .json(®ister::Payload { + username, + password: Secret::new("verysecure"), + }) + .send() + .await + .unwrap(); + + resp.json::().await +} + +#[tokio::test] +async fn register_test() { + let client = Env::new().await; + + let resp = register(&client).await.unwrap(); + + tracing::info!(?resp); + + assert!(resp.access_token.is_some() && resp.access_token.map(|at| !at.is_empty()).unwrap()); +} diff --git a/crates/test/src/env.rs b/crates/test/src/env.rs new file mode 100644 index 0000000..3407572 --- /dev/null +++ b/crates/test/src/env.rs @@ -0,0 +1,66 @@ +use std::net::SocketAddr; + +pub(crate) struct Env { + pub client: reqwest::Client, + pub loopback: SocketAddr, +} + +impl Env { + pub(crate) async fn new() -> Self { + let _ = tracing_subscriber::fmt().try_init(); + + commune::init().await; + + let loopback = SocketAddr::from(( + match commune::commune().config.public_loopback { + true => [0, 0, 0, 0], + false => [127, 0, 0, 1], + }, + 5357, + )); + + tokio::spawn(async move { + tracing::info!("starting development server on {:?}", loopback); + + router::serve(commune::commune().config.public_loopback, 5357) + .await + .expect("failed to bind to address"); + }); + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + if let Err(e) = client + .get(commune::commune().config.matrix.host.to_string() + "/_matrix/client/versions") + .send() + .await + { + tracing::error!( + "could not connect to Matrix: {e}\n is the testing environment running?" + ); + + std::process::exit(1); + } + + Self { client, loopback } + } + + fn path(&self, path: &str) -> String { + format!("http://{}{}", self.loopback, path) + } + + #[allow(dead_code)] + pub(crate) fn get(&self, url: &str) -> reqwest::RequestBuilder { + tracing::info!("GET {}", self.path(url)); + + self.client.get(self.path(url)) + } + + pub(crate) fn post(&self, url: &str) -> reqwest::RequestBuilder { + tracing::info!("POST {}", self.path(url)); + + self.client.post(self.path(url)) + } +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs new file mode 100644 index 0000000..c84f5e1 --- /dev/null +++ b/crates/test/src/lib.rs @@ -0,0 +1,17 @@ +// #[cfg(test)] +// mod commune; + +// #[cfg(test)] +// mod tools; + +// #[cfg(test)] +// mod matrix; + +// #[cfg(test)] +// mod server; + +#[cfg(test)] +mod api; + +#[cfg(test)] +mod env; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..18a4f56 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +version: '3' + +services: + mailcrab: + image: marlonb/mailcrab:latest + ports: + - '1025:1025' + networks: [default] + + redis: + image: 'redis/redis-stack' + ports: + - '6379:6379' + - '8001:8001' + volumes: + - redis-db:/data + + synapse-db: + image: 'postgres:16' + ports: + - '5432:5432' + volumes: + - synapse-db:/var/lib/postgresql/data + env_file: + - .env + restart: always + + synapse: + image: 'ghcr.io/element-hq/synapse:v1.100.0' + user: "${DOCKER_USER}" + ports: + - '8008:8008' + - '8448:8448' + volumes: + - ./docker/synapse:/data + env_file: + - .env + restart: always + network_mode: 'host' + depends_on: + - synapse-db + +volumes: + redis-db: + synapse-db: diff --git a/docs/diagrams/diagram.excalidraw b/docs/diagrams/diagram.excalidraw new file mode 100644 index 0000000..eaa5793 --- /dev/null +++ b/docs/diagrams/diagram.excalidraw @@ -0,0 +1,475 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "RYL6z1yNI4ryhDrjxemyL", + "type": "rectangle", + "x": 364, + "y": 272, + "width": 130, + "height": 119, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1352799417, + "version": 94, + "versionNonce": 297889623, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "NwNRXHacaPiTQKk-sS3iv" + }, + { + "id": "jR76iIplo4TVvNKZBH6vT", + "type": "arrow" + } + ], + "updated": 1700421911263, + "link": null, + "locked": false + }, + { + "id": "NwNRXHacaPiTQKk-sS3iv", + "type": "text", + "x": 393.84375, + "y": 319.5, + "width": 70.3125, + "height": 24, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 683172535, + "version": 80, + "versionNonce": 581421529, + "isDeleted": false, + "boundElements": null, + "updated": 1700421905506, + "link": null, + "locked": false, + "text": "Client", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 19, + "containerId": "RYL6z1yNI4ryhDrjxemyL", + "originalText": "Client", + "lineHeight": 1.2 + }, + { + "type": "rectangle", + "version": 207, + "versionNonce": 145190007, + "isDeleted": false, + "id": "igO3c0IESW8ZSDcllatP-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 582, + "y": 269.5, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 130, + "height": 119, + "seed": 2026968473, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "E1cQEf95N_pZWvm755-z4" + }, + { + "id": "jR76iIplo4TVvNKZBH6vT", + "type": "arrow" + }, + { + "id": "I--VThaGzeWEpMH77IyuH", + "type": "arrow" + } + ], + "updated": 1700421911263, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 206, + "versionNonce": 648226489, + "isDeleted": false, + "id": "E1cQEf95N_pZWvm755-z4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 605.984375, + "y": 305, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffec99", + "width": 82.03125, + "height": 48, + "seed": 815919225, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1700421905506, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Commune\nServer", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "igO3c0IESW8ZSDcllatP-", + "originalText": "Commune\nServer", + "lineHeight": 1.2, + "baseline": 43 + }, + { + "id": "jR76iIplo4TVvNKZBH6vT", + "type": "arrow", + "x": 495, + "y": 328.38774886792805, + "width": 86, + "height": 0.220972375229735, + "angle": 0, + "strokeColor": "#fa5252", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1227684473, + "version": 56, + "versionNonce": 1745929623, + "isDeleted": false, + "boundElements": null, + "updated": 1700421911263, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 86, + -0.220972375229735 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "RYL6z1yNI4ryhDrjxemyL", + "focus": -0.04931816566337021, + "gap": 1 + }, + "endBinding": { + "elementId": "igO3c0IESW8ZSDcllatP-", + "focus": 0.016806722689075442, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "triangle" + }, + { + "id": "BBin9aP3kMIsghlY9Q3Xb", + "type": "rectangle", + "x": 803, + "y": 235, + "width": 305, + "height": 200, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1920549721, + "version": 263, + "versionNonce": 2133981879, + "isDeleted": false, + "boundElements": [ + { + "id": "I--VThaGzeWEpMH77IyuH", + "type": "arrow" + } + ], + "updated": 1700421911263, + "link": null, + "locked": false + }, + { + "id": "mxgPLlWP_Rswux186mo6F", + "type": "image", + "x": 906, + "y": 149, + "width": 85, + "height": 85, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1091137593, + "version": 301, + "versionNonce": 1118800855, + "isDeleted": false, + "boundElements": null, + "updated": 1700421911263, + "link": null, + "locked": false, + "status": "saved", + "fileId": "674c6c1c00fa6878a440ca4cb9c65ee6ce0b9d51", + "scale": [ + 1, + 1 + ] + }, + { + "id": "4N7vb2roINeGhvAK-EnUr", + "type": "rectangle", + "x": 831, + "y": 278, + "width": 114, + "height": 108, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 717291801, + "version": 341, + "versionNonce": 513669367, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "4YZ1HMXFK8M48eFnDMR4v" + } + ], + "updated": 1700421911263, + "link": null, + "locked": false + }, + { + "id": "4YZ1HMXFK8M48eFnDMR4v", + "type": "text", + "x": 846.984375, + "y": 308, + "width": 82.03125, + "height": 48, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1393907641, + "version": 262, + "versionNonce": 1356851097, + "isDeleted": false, + "boundElements": null, + "updated": 1700421905507, + "link": null, + "locked": false, + "text": "Matrix\nSynapse", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 43, + "containerId": "4N7vb2roINeGhvAK-EnUr", + "originalText": "Matrix\nSynapse", + "lineHeight": 1.2 + }, + { + "type": "rectangle", + "version": 494, + "versionNonce": 142721559, + "isDeleted": false, + "id": "9lVssKY2uk0fr5TCbd5kT", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 969, + "y": 277, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "width": 114, + "height": 108, + "seed": 487814649, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "QJwL5UFm3QWsAGT_IMZZG" + } + ], + "updated": 1700421911263, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 431, + "versionNonce": 462523513, + "isDeleted": false, + "id": "QJwL5UFm3QWsAGT_IMZZG", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 979.125, + "y": 321.4, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "width": 93.75, + "height": 19.2, + "seed": 1463634583, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1700421905508, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "PostgreSQL", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "9lVssKY2uk0fr5TCbd5kT", + "originalText": "PostgreSQL", + "lineHeight": 1.2, + "baseline": 15 + }, + { + "type": "arrow", + "version": 257, + "versionNonce": 1364158263, + "isDeleted": false, + "id": "I--VThaGzeWEpMH77IyuH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 714, + "y": 329.467361039966, + "strokeColor": "#e8590c", + "backgroundColor": "#a5d8ff", + "width": 86, + "height": 0.2401904503404353, + "seed": 184831705, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1700421911263, + "link": null, + "locked": false, + "startBinding": { + "elementId": "igO3c0IESW8ZSDcllatP-", + "gap": 2, + "focus": 0.010959919320073687 + }, + "endBinding": { + "elementId": "BBin9aP3kMIsghlY9Q3Xb", + "gap": 3, + "focus": 0.06180802042331033 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 86, + -0.2401904503404353 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "674c6c1c00fa6878a440ca4cb9c65ee6ce0b9d51": { + "mimeType": "image/png", + "id": "674c6c1c00fa6878a440ca4cb9c65ee6ce0b9d51", + "dataURL": "data:image/png;base64,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", + "created": 1700421758712, + "lastRetrieved": 1700421758712 + } + } +} \ No newline at end of file diff --git a/docs/diagrams/diagram.png b/docs/diagrams/diagram.png new file mode 100644 index 0000000..bb027bd Binary files /dev/null and b/docs/diagrams/diagram.png differ diff --git a/fixtures/generate_mac.py b/fixtures/generate_mac.py new file mode 100644 index 0000000..d917188 --- /dev/null +++ b/fixtures/generate_mac.py @@ -0,0 +1,32 @@ +import hmac, hashlib + +def generate_mac(nonce, user, password, admin=False, user_type=None): + + mac = hmac.new( + key=b"m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B", + digestmod=hashlib.sha1, + ) + + mac.update(nonce.encode('utf8')) + mac.update(b"\x00") + mac.update(user.encode('utf8')) + mac.update(b"\x00") + mac.update(password.encode('utf8')) + mac.update(b"\x00") + mac.update(b"admin" if admin else b"notadmin") + if user_type: + mac.update(b"\x00") + mac.update(user_type.encode('utf8')) + + return mac.hexdigest() + +if __name__ == "__main__": + mac = generate_mac( + nonce="1234567890", + user="groot", + password="imroot!1234", + admin=True, + user_type=None + ) + + print(mac) diff --git a/src/api.rs b/src/api.rs deleted file mode 100644 index c3e15b5..0000000 --- a/src/api.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod ping; - -pub(crate) use ping::ping_route; diff --git a/src/api/ping.rs b/src/api/ping.rs deleted file mode 100644 index 64ae3ad..0000000 --- a/src/api/ping.rs +++ /dev/null @@ -1,20 +0,0 @@ -use axum::{response::IntoResponse, Json, RequestExt}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize)] -#[allow(dead_code)] -pub(crate) struct Request { - #[serde(rename = "transaction_id")] - txn_id: String, -} - -#[derive(Serialize)] -#[allow(dead_code)] -pub(crate) struct Response {} - -pub(crate) async fn ping_route(request: axum::extract::Request) -> impl IntoResponse { - let _ = request - .extract::, _>() - .await - .map_err(|_error| {}); -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index c05b5dc..0000000 --- a/src/main.rs +++ /dev/null @@ -1,7 +0,0 @@ -use axum::{routing::post, Router}; - -pub(crate) mod api; - -fn main() { - let _router = Router::<()>::new().route("/ping", post(api::ping_route)); -}