diff --git a/.env.docker.sample b/.env.docker.sample index 7600a38..a909ec8 100644 --- a/.env.docker.sample +++ b/.env.docker.sample @@ -8,6 +8,6 @@ DB_MAIN_SSLMODE=disable DATABASE_URL=postgres://${DB_MAIN_USER}:${DB_MAIN_PASSWORD}@${DB_MAIN_HOST}:${DB_MAIN_PORT}/${DB_MAIN_NAME} TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T -TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" +TWILIO_AUTH_TOKEN="MA08HJp9tYtkfs72m8JIIROofRVm74oR0ixEYBHGYpB9" TWILIO_PHONE_NUMBER=+10123456789 ALLOWED_ORIGINS="http://textsender.com" diff --git a/.env.local.sample b/.env.local.sample index 166eb33..339c1d2 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -8,6 +8,6 @@ DB_MAIN_SSLMODE=disable DATABASE_URL=postgres://${DB_MAIN_USER}:${DB_MAIN_PASSWORD}@${DB_MAIN_HOST}:${DB_MAIN_PORT}/${DB_MAIN_NAME} TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T -TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" +TWILIO_AUTH_TOKEN="MA08HJp9tYtkfs72m8JIIROofRVm74oR0ixEYBHGYpB9" TWILIO_PHONE_NUMBER=+10123456789 ALLOWED_ORIGINS="http://textsender.com" diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..7f58e3f --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,91 @@ +name: Rust Build + +on: + pull_request: + branches: + - next + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test Suite + runs-on: ubuntu-24.04 + # --- Add database service definition --- + services: + postgres: + image: postgres:18.4-alpine + env: + # Use secrets for DB init, with fallbacks for flexibility + POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }} + POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }} + POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }} + POSTGRES_PORT: ${{ secrets.DB_PORT || 5432 }} + # Options to wait until the database is ready + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.96 + # --- Add this step for explicit verification --- + - name: Verify Docker Environment + run: | + echo "Runner User Info:" + id + echo "Checking Docker Version:" + docker --version + echo "Checking Docker Daemon Status (info):" + docker info + echo "Checking Docker Daemon Status (ps):" + docker ps -a + echo "Docker environment check complete." + # NOTE: Do NOT use continue-on-error here. + # If Docker isn't working as expected, the job SHOULD fail here. + - name: Run tests + env: + # Define DATABASE_URL for tests to use + DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:${{ secrets.DB_PORT || 5432 }}/${{ secrets.DB_TEST_NAME || 'testdb' }} + RUST_LOG: info # Optional: configure test log level + SECRET_MAIN_KEY: ${{ secrets.SECRET_KEY }} + # Make SSH agent available if tests fetch private dependencies + SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }} + ENABLE_REGISTRATION: 'TRUE' + run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + + cargo test + + fmt: + name: Rustfmt + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.96 + - run: rustup component add rustfmt + - uses: Swatinem/rust-cache@v2 + - run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + cargo fmt --all -- --check diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index 634616a..bdb4abd 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -37,7 +37,7 @@ jobs: # --- Add database service definition --- services: postgres: - image: postgres:18.3-alpine + image: postgres:18.4-alpine env: # Use secrets for DB init, with fallbacks for flexibility POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }} diff --git a/Cargo.lock b/Cargo.lock index dc3eaa9..0fe8c99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -59,6 +59,28 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.9" @@ -146,6 +168,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-ng" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735259e24200bce69feb75fcb2f79f36129e54c3f3139da15880520988cd840c" + [[package]] name = "base64ct" version = "1.8.3" @@ -170,6 +198,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -184,17 +221,19 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cc" -version = "1.2.64" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -205,19 +244,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "common-multipart-rfc7578" -version = "0.7.0" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08d53b5e0c302c5830cfa7511ba0edc3f241c691a95c0d184dfb761e11a6cc2" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", - "futures-core", - "futures-util", - "http", - "mime", - "mime_guess", - "rand", - "thiserror 1.0.69", + "memchr", ] [[package]] @@ -267,6 +332,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -292,6 +367,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -338,7 +422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -353,6 +437,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -360,9 +462,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -417,12 +519,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -440,6 +553,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.16.9" @@ -447,7 +566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -473,7 +592,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -495,14 +614,14 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", - "hkdf", + "hkdf 0.12.4", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -544,6 +663,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -567,7 +696,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -605,12 +734,35 @@ dependencies = [ "spin", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -635,6 +787,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -752,21 +910,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasip2", - "wasip3", + "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -776,10 +949,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -788,7 +980,18 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -806,6 +1009,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -834,7 +1046,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -843,7 +1064,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -900,6 +1130,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -910,6 +1149,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -918,6 +1158,22 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] @@ -926,13 +1182,23 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1017,12 +1283,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -1056,12 +1316,77 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "josekit" version = "0.10.3" @@ -1075,15 +1400,15 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "time", ] [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -1099,16 +1424,16 @@ dependencies = [ "base64", "ed25519-dalek", "getrandom 0.2.17", - "hmac", + "hmac 0.12.1", "js-sys", "p256", "p384", "pem", - "rand", + "rand 0.8.6", "rsa", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "signature", "simple_asn1", "zeroize", @@ -1138,12 +1463,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -1201,9 +1520,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "matchit" @@ -1218,7 +1543,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", ] [[package]] @@ -1328,7 +1663,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1427,7 +1762,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1439,7 +1774,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1559,16 +1894,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -1588,14 +1913,76 @@ dependencies = [ ] [[package]] -name = "quote" -version = "1.0.45" +name = "quinn" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1609,8 +1996,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1620,7 +2028,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1632,6 +2050,21 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1679,16 +2112,76 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.10" @@ -1696,13 +2189,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", - "digest", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1739,10 +2232,16 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1765,6 +2264,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1822,7 +2396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1918,8 +2492,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -1929,8 +2514,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -1964,8 +2560,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -1974,6 +2570,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple_asn1" version = "0.6.4" @@ -1982,7 +2594,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -2036,11 +2648,24 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", + "sqlx-core 0.8.6", + "sqlx-macros 0.8.6", + "sqlx-mysql 0.8.6", + "sqlx-postgres 0.8.6", + "sqlx-sqlite 0.8.6", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core 0.9.0", + "sqlx-macros 0.9.0", + "sqlx-mysql 0.9.0", + "sqlx-postgres 0.9.0", + "sqlx-sqlite 0.9.0", ] [[package]] @@ -2060,7 +2685,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink", + "hashlink 0.10.0", "indexmap", "log", "memchr", @@ -2069,9 +2694,46 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", - "thiserror 2.0.18", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink 0.11.1", + "indexmap", + "log", + "memchr", + "native-tls", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", "time", "tokio", "tokio-stream", @@ -2088,8 +2750,21 @@ checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", - "sqlx-core", - "sqlx-macros-core", + "sqlx-core 0.8.6", + "sqlx-macros-core 0.8.6", + "syn", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core 0.9.0", + "sqlx-macros-core 0.9.0", "syn", ] @@ -2108,16 +2783,42 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", + "sha2 0.10.9", + "sqlx-core 0.8.6", + "sqlx-mysql 0.8.6", + "sqlx-postgres 0.8.6", + "sqlx-sqlite 0.8.6", "syn", "tokio", "url", ] +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core 0.9.0", + "sqlx-mysql 0.9.0", + "sqlx-postgres 0.9.0", + "sqlx-sqlite 0.9.0", + "syn", + "thiserror", + "tokio", + "url", +] + [[package]] name = "sqlx-mysql" version = "0.8.6" @@ -2130,7 +2831,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -2139,27 +2840,55 @@ dependencies = [ "futures-util", "generic-array", "hex", - "hkdf", - "hmac", + "hkdf 0.12.4", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.6", "rsa", "serde", - "sha1", - "sha2", + "sha1 0.10.6", + "sha2 0.10.9", "smallvec", - "sqlx-core", + "sqlx-core 0.8.6", "stringprep", - "thiserror 2.0.18", + "thiserror", + "time", + "tracing", + "uuid", + "whoami 1.6.1", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core 0.9.0", + "thiserror", "time", "tracing", "uuid", - "whoami", ] [[package]] @@ -2174,31 +2903,68 @@ dependencies = [ "byteorder", "crc", "dotenvy", - "etcetera", + "etcetera 0.8.0", "futures-channel", "futures-core", "futures-util", "hex", - "hkdf", - "hmac", + "hkdf 0.12.4", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", - "rand", + "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", - "sqlx-core", + "sqlx-core 0.8.6", "stringprep", - "thiserror 2.0.18", + "thiserror", "time", "tracing", "uuid", - "whoami", + "whoami 1.6.1", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera 0.11.0", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf 0.13.0", + "hmac 0.13.0", + "itoa", + "log", + "md-5 0.11.0", + "memchr", + "rand 0.10.1", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core 0.9.0", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami 2.1.2", ] [[package]] @@ -2208,7 +2974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", - "flume", + "flume 0.11.1", "futures-channel", "futures-core", "futures-executor", @@ -2219,8 +2985,34 @@ dependencies = [ "percent-encoding", "serde", "serde_urlencoded", - "sqlx-core", - "thiserror 2.0.18", + "sqlx-core 0.8.6", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume 0.12.0", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core 0.9.0", + "thiserror", "time", "tracing", "url", @@ -2250,11 +3042,31 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swoosh" +version = "0.4.2" +source = "git+ssh://git@git.kundeng.us/phoenix/swoosh.git?tag=v0.4.2.0#e1e730fd1cac720e03f7b1d60ddba992d3b09a9d" +dependencies = [ + "base64-ng", + "const_format", + "futures", + "http", + "josekit", + "rand 0.10.1", + "reqwest", + "serde", + "serde_json", + "textsender_models", + "time", + "tokio", + "uuid", +] + [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2266,6 +3078,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2278,6 +3093,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -2285,7 +3121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2293,22 +3129,19 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.7" +version = "0.2.0" dependencies = [ "axum", "axum-extra", - "base64", - "common-multipart-rfc7578", "futures", "josekit", "jsonwebtoken", - "mime_guess", "serde", "serde_json", - "sqlx", + "sqlx 0.9.0", + "swoosh", "tempfile", "textsender_models", - "thiserror 2.0.18", "time", "tokio", "tokio-util", @@ -2323,46 +3156,27 @@ dependencies = [ [[package]] name = "textsender_models" -version = "0.4.0" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.0#2722af5589023db819554a97c1f596d9e735fe96" +version = "0.4.10" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.10#c1d391939baf94df09ee405a1f0bf269690f7dec" dependencies = [ "const_format", "dotenvy", "josekit", "serde", "serde_json", + "sqlx 0.8.6", "time", "utoipa", "uuid", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2387,9 +3201,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.49" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", "num-conv", @@ -2407,9 +3221,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.29" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -2468,6 +3282,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2520,12 +3344,15 @@ checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", + "futures-util", "http", "http-body", "pin-project-lite", "tokio", + "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2598,6 +3425,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.1" @@ -2643,6 +3476,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2706,11 +3545,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -2744,6 +3583,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2756,16 +3604,7 @@ version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -2776,9 +3615,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2788,10 +3627,20 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.125" +name = "wasm-bindgen-futures" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2799,9 +3648,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -2812,45 +3661,53 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "wasm-streams" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ - "leb128fmt", - "wasmparser", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "web-sys" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", ] [[package]] @@ -2863,6 +3720,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2878,13 +3741,60 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -2902,13 +3812,46 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -2917,36 +3860,120 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2954,13 +3981,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" @@ -2968,85 +3998,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -3186,9 +4137,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index ccf8ec5..4d2860e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.7" +version = "0.2.0" edition = "2024" rust-version = "1.96" @@ -15,19 +15,16 @@ tower = { version = "0.5.3", features = ["full"] } tower-http = { version = "0.6.11", features = ["cors", "timeout"] } tracing-subscriber = "0.3.23" futures = { version = "0.3.32" } -mime_guess = { version = "2.0.5" } uuid = { version = "1.23.3", features = ["v4", "serde"] } -sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } +sqlx = { version = "0.9.0", features = ["runtime-tokio", "tls-native-tls", "postgres", "time", "uuid"] } time = { version = "0.3.49", features = ["formatting", "macros", "parsing", "serde"] } -thiserror = "2.0.18" -base64 = "0.22.1" jsonwebtoken = { version = "10.4.0", features = ["rust_crypto"] } josekit = { version = "0.10.3" } utoipa = { version = "5.5.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.4.0" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.4.10" } +swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.4.2.0" } [dev-dependencies] -common-multipart-rfc7578 = { version = "0.7.0" } url = { version = "2.5.8" } tempfile = { version = "3.27.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 7fe4794..6dcd817 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,28 +45,28 @@ services: # - ./path/to/your/web-api-repo:/app # --- catapult service --- -# catapult: -# build: -# context: ../catapult -# ssh: ["default"] -# dockerfile: Dockerfile -# container_name: catapult -# restart: unless-stopped -# env_file: -# - ../catapult/.env -# depends_on: -# - api -# - main_db -# - auth_api -# - auth_db -# networks: -# - textsender_api-network + catapult: + build: + context: ../catapult + ssh: ["default"] + dockerfile: Dockerfile + container_name: catapult + restart: unless-stopped + env_file: + - ../catapult/.env + depends_on: + - api + - main_db + - auth_api + - auth_db + networks: + - textsender_api-network # PostgreSQL Database Service # --- textsender_api web api db --- main_db: - image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller) + image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller) container_name: textsender_api_db # Optional: Give the container a specific name environment: # These MUST match the user, password, and database name in the DATABASE_URL above diff --git a/migrations/20260613211440_init.sql b/migrations/20260613211440_init.sql index ef5cc2b..7be9c7b 100644 --- a/migrations/20260613211440_init.sql +++ b/migrations/20260613211440_init.sql @@ -10,3 +10,36 @@ CREATE TABLE IF NOT EXISTS "contacts" ( lastname TEXT NULL, nickname TEXT NULL ); + +CREATE TABLE IF NOT EXISTS "messages" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + content TEXT NOT NULL, + user_id UUID NOT NULL +); + +CREATE TABLE IF NOT EXISTS "scheduled_messages" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + scheduled timestamptz NOT NULL, + created timestamptz DEFAULT now(), + status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')), + user_id UUID NOT NULL +); + +CREATE TABLE IF NOT EXISTS "scheduled_message_events" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + contact_id UUID NOT NULL, + message_id UUID NOT NULL, + scheduled_message_id UUID NOT NULL, + created timestamptz DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS "message_event_responses" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + scheduled_message_event_id UUID NULL, + response JSONB NOT NULL, + user_id UUID NOT NULL, + sent timestamptz NOT NULL, + contact_id UUID NULL, + message_id UUID NULL, + status TEXT CHECK (status IN ('INSTANT', 'SCHEDULED')) +); diff --git a/migrations/schema.sql b/migrations/schema.sql deleted file mode 100644 index 9ca3c39..0000000 --- a/migrations/schema.sql +++ /dev/null @@ -1,49 +0,0 @@ --- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; --- --- DROP TABLE IF EXISTS contacts CASCADE; --- DROP TABLE IF EXISTS messages CASCADE; --- DROP TABLE IF EXISTS scheduled_messages CASCADE; --- DROP TABLE IF EXISTS scheduled_message_events CASCADE; --- DROP TABLE IF EXISTS message_event_responses CASCADE; --- --- CREATE TABLE IF NOT EXISTS contacts ( --- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), --- phone_number TEXT NOT NULL, --- user_id UUID NOT NULL, --- first_name TEXT NULL, --- last_name TEXT NULL, --- nickname TEXT NULL --- ); --- --- CREATE TABLE IF NOT EXISTS messages ( --- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), --- content TEXT NOT NULL, --- user_id UUID NOT NULL --- ); --- --- CREATE TABLE IF NOT EXISTS scheduled_messages ( --- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), --- scheduled timestamptz NOT NULL, --- created timestamptz DEFAULT now(), --- status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')), --- user_id UUID NOT NULL --- ); --- --- CREATE TABLE IF NOT EXISTS scheduled_message_events ( --- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), --- contact_id UUID NOT NULL, --- message_id UUID NOT NULL, --- scheduled_message_id UUID NOT NULL, --- created timestamptz DEFAULT now() --- ); --- --- CREATE TABLE IF NOT EXISTS message_event_responses ( --- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), --- scheduled_message_event_id UUID NULL, --- response JSONB NOT NULL, --- user_id UUID NOT NULL, --- sent timestamptz NOT NULL, --- contact_id UUID NULL, --- message_id UUID NULL, --- status TEXT CHECK (status IN ('INSTANT', 'SCHEDULED')) --- ); diff --git a/src/caller/contact.rs b/src/caller/contact.rs index 3b07568..6093df5 100644 --- a/src/caller/contact.rs +++ b/src/caller/contact.rs @@ -171,7 +171,7 @@ pub mod endpoint { } } - // Endpoint to get songs + /// Endpoint to get Contacts #[utoipa::path( get, path = super::super::endpoints::GET_CONTACT, diff --git a/src/caller/instant.rs b/src/caller/instant.rs new file mode 100644 index 0000000..4b1fcb8 --- /dev/null +++ b/src/caller/instant.rs @@ -0,0 +1,171 @@ +pub mod request { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct InstantMessageRequest { + pub contact_ids: Vec, + pub message_id: uuid::Uuid, + pub user_id: uuid::Uuid, + } + + impl InstantMessageRequest { + pub fn is_valid(&self) -> bool { + !self.contact_ids.is_empty() || !self.message_id.is_nil() || !self.user_id.is_nil() + } + } +} + +pub mod response { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct InstantMessageResponse { + pub message: String, + pub data: Vec, + } +} + +pub mod endpoint { + use crate::repo::contact as contact_repo; + use crate::repo::message as message_repo; + use message_repo::event as event_repo; + + /// Endpoint to create Message + #[utoipa::path( + post, + path = super::super::endpoints::ADD_MESSAGE, + request_body( + content = super::request::InstantMessageRequest, + description = "Data needed to create a Message", + content_type = "application/json" + ), + responses( + (status = 201, description = "Message created", body = super::response::InstantMessageResponse), + (status = 400, description = "Error", body = super::response::InstantMessageResponse), + (status = 500, description = "Error creating Message", body = super::response::InstantMessageResponse) + ) +)] + pub async fn send_message( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::InstantMessageResponse::default(); + + if payload.is_valid() { + let mut contacts: Vec = Vec::new(); + + for contact_id in &payload.contact_ids { + match contact_repo::get(&pool, contact_id).await { + Ok(contact) => { + contacts.push(contact); + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Invalid"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + } + } + + if !contacts.is_empty() { + println!("Valid contacts"); + + match message_repo::get(&pool, &payload.message_id).await { + Ok(message) => { + println!("Valid message"); + let t_config = + match textsender_models::config::auxiliary::load_config().await { + Ok(config) => config, + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Error getting config"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }; + let mut results: Vec< + textsender_models::message::event::MessageEventResponse, + > = Vec::new(); + let pp = swoosh::twilio::types::Parameters { + schedule: false, + schedule_at: None, + }; + + for contact in &contacts { + match swoosh::twilio::api::send_message( + &message, contact, &pp, &t_config, + ) + .await + { + Ok(result) => { + let val = swoosh::twilio::api::response_to_json(result).await; + let mer = textsender_models::message::event::MessageEventResponse { + user_id: payload.user_id, + // TODO: Make this more accurate + sent: Some(time::OffsetDateTime::now_utc()), + status: String::from(textsender_models::message::event::MESSAGE_EVENT_RESPONSE_STATUS_INSTANT), + contact_id: contact.id.unwrap(), + message_id: message.id.unwrap(), + response: val, + ..Default::default() + }; + results.push(mer); + } + Err(err) => { + eprintln!("Error: {err:?}"); + } + } + } + + if results.is_empty() { + eprintln!("Error sending"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } else { + for result in &mut results { + match event_repo::insert(&pool, result).await { + Ok(i) => { + result.id = i; + } + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + } + } + response.data = results; + response.message = String::from(super::super::response::SUCCESSFUL); + (axum::http::StatusCode::OK, axum::Json(response)) + } + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } +} diff --git a/src/caller/message/event.rs b/src/caller/message/event.rs new file mode 100644 index 0000000..a3b79c4 --- /dev/null +++ b/src/caller/message/event.rs @@ -0,0 +1,167 @@ +pub mod request { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct RecordMessageEventRequest { + pub scheduled_message_event_id: uuid::Uuid, + pub response: serde_json::Value, + pub user_id: uuid::Uuid, + #[serde(with = "time::serde::rfc3339::option")] + pub sent: Option, + pub status: String, + pub contact_id: uuid::Uuid, + pub message_id: uuid::Uuid, + } + + impl RecordMessageEventRequest { + pub fn is_valid(&self) -> bool { + !self.scheduled_message_event_id.is_nil() + || !self.response.is_null() + || !self.user_id.is_nil() + || self.sent.is_some() + || !self.status.is_empty() + || !self.contact_id.is_nil() + || !self.message_id.is_nil() + } + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] + pub struct GetMessageEventResponseParams { + pub id: Option, + pub user_id: Option, + } +} + +pub mod response { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct RecordMessageEventResponse { + pub message: String, + pub data: Vec, + } + + pub use RecordMessageEventResponse as GetMERResponse; +} + +pub mod endpoint { + use crate::repo::message::event as event_repo; + + /// Endpoint to create a Message Event Response + #[utoipa::path( + post, + path = crate::caller::endpoints::RECORD_MESSAGE_EVENT_RESPONSE, + request_body( + content = super::request::RecordMessageEventRequest, + description = "Data needed to create a Message Event Response", + content_type = "application/json" + ), + responses( + (status = 201, description = "Message Event Response created", body = super::response::RecordMessageEventResponse), + (status = 400, description = "Error", body = super::response::RecordMessageEventResponse), + (status = 500, description = "Error creating Message Event Response", body = super::response::RecordMessageEventResponse) + ) +)] + pub async fn record_message_event_response( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::RecordMessageEventResponse::default(); + + if payload.is_valid() { + let mut mer = textsender_models::message::event::MessageEventResponse { + scheduled_message_event_id: Some(payload.scheduled_message_event_id), + user_id: payload.user_id, + sent: payload.sent, + status: payload.status, + contact_id: payload.contact_id, + message_id: payload.message_id, + response: payload.response, + ..Default::default() + }; + + match event_repo::insert(&pool, &mer).await { + Ok(id) => { + mer.id = id; + response.message = String::from(super::super::super::response::SUCCESSFUL); + response.data.push(mer); + (axum::http::StatusCode::CREATED, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + + /// Endpoint to get Messages + #[utoipa::path( + get, + path = crate::caller::endpoints::GET_MESSAGE_EVENT_RESPONSE, + params( + ("id" = uuid::Uuid, Path, description = "Id of Message Event Response"), + ("user_id" = uuid::Uuid, Path, description = "User Id associated with the Message Event Response") + ), + responses( + (status = 200, description = "Message Event Response found", body = super::response::GetMERResponse), + (status = 400, description = "Error getting Message Event Response", body = super::response::GetMERResponse) + ) + )] + pub async fn get_record_message_event_response( + axum::Extension(pool): axum::Extension, + axum::extract::Query(params): axum::extract::Query< + super::request::GetMessageEventResponseParams, + >, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::GetMERResponse::default(); + + let messages = match params.id { + Some(id) => match event_repo::get(&pool, &id).await { + Ok(message) => { + vec![message] + } + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => match params.user_id { + Some(user_id) => match event_repo::get_with_user_id(&pool, &user_id).await { + Ok(messages) => messages, + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => { + response.message = String::from("Invalid parameter"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + }, + }; + + response.data = messages; + response.message = String::from(super::super::super::response::SUCCESSFUL); + + (axum::http::StatusCode::OK, axum::Json(response)) + } +} diff --git a/src/caller/message/mod.rs b/src/caller/message/mod.rs new file mode 100644 index 0000000..5f1ba57 --- /dev/null +++ b/src/caller/message/mod.rs @@ -0,0 +1,154 @@ +pub mod event; +pub mod scheduling; + +pub mod request { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct AddMessageRequest { + pub content: String, + pub user_id: uuid::Uuid, + } + + impl AddMessageRequest { + pub fn is_valid(&self) -> bool { + !self.content.is_empty() || !self.user_id.is_nil() + } + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] + pub struct GetMessageParams { + pub id: Option, + pub user_id: Option, + } +} + +pub mod response { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct AddMessageResponse { + pub message: String, + pub data: Vec, + } + + pub use AddMessageResponse as GetMessageResponse; +} + +pub mod endpoint { + use crate::repo::message as message_repo; + + /// Endpoint to create Message + #[utoipa::path( + post, + path = super::super::endpoints::ADD_MESSAGE, + request_body( + content = super::request::AddMessageRequest, + description = "Data needed to create a Message", + content_type = "application/json" + ), + responses( + (status = 201, description = "Message created", body = super::response::AddMessageResponse), + (status = 400, description = "Error", body = super::response::AddMessageResponse), + (status = 500, description = "Error creating Message", body = super::response::AddMessageResponse) + ) +)] + pub async fn create_message( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::AddMessageResponse::default(); + + if payload.is_valid() { + let mut msg = textsender_models::message::Message { + content: payload.content, + user_id: Some(payload.user_id), + ..Default::default() + }; + match message_repo::insert(&pool, &msg, &payload.user_id).await { + Ok(message_id) => { + msg.id = Some(message_id); + response.data.push(msg); + response.message = String::from("Message created"); + (axum::http::StatusCode::CREATED, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Contact not created"); + + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + + /// Endpoint to get Messages + #[utoipa::path( + get, + path = super::super::endpoints::GET_MESSAGE, + params( + ("id" = uuid::Uuid, Path, description = "Id of Message"), + ("user_id" = uuid::Uuid, Path, description = "User Id associated with the Message") + ), + responses( + (status = 200, description = "Songs found", body = super::response::GetMessageResponse), + (status = 400, description = "Error getting songs", body = super::response::GetMessageResponse) + ) + )] + pub async fn get_messages( + axum::Extension(pool): axum::Extension, + axum::extract::Query(params): axum::extract::Query, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::GetMessageResponse::default(); + + let messages = match params.id { + Some(id) => match message_repo::get(&pool, &id).await { + Ok(message) => { + vec![message] + } + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => match params.user_id { + Some(user_id) => match message_repo::get_with_user_id(&pool, &user_id).await { + Ok(messages) => messages, + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => { + response.message = String::from("Invalid parameter"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + }, + }; + + response.data = messages; + response.message = String::from(super::super::response::SUCCESSFUL); + + (axum::http::StatusCode::OK, axum::Json(response)) + } +} diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs new file mode 100644 index 0000000..d146889 --- /dev/null +++ b/src/caller/message/scheduling.rs @@ -0,0 +1,549 @@ +pub mod request { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct ScheduleMessageRequest { + #[serde(with = "time::serde::rfc3339::option")] + pub scheduled: Option, + pub status: String, + pub user_id: uuid::Uuid, + } + + impl ScheduleMessageRequest { + pub fn is_valid(&self) -> bool { + self.scheduled.is_some() || !self.status.is_empty() || !self.user_id.is_nil() + } + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] + pub struct GetScheduledMessageParams { + pub id: Option, + pub user_id: Option, + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] + pub struct CreateScheduleMessageEventRequest { + pub contact_id: uuid::Uuid, + pub message_id: uuid::Uuid, + pub scheduled_message_id: uuid::Uuid, + } + + impl CreateScheduleMessageEventRequest { + pub fn is_valid(&self) -> bool { + !self.contact_id.is_nil() + || !self.message_id.is_nil() + || !self.scheduled_message_id.is_nil() + } + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] + pub struct GetScheduledMessageEventParams { + pub id: Option, + pub scheduled_message_id: Option, + } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct UpdateScheduledMessageStatusRequest { + pub status: String, + pub scheduled_message_id: uuid::Uuid, + } + + impl UpdateScheduledMessageStatusRequest { + pub fn is_valid(&self) -> bool { + if !self.status.is_empty() || !self.scheduled_message_id.is_nil() { + self.status == textsender_models::message::scheduling::PENDING + || self.status == textsender_models::message::scheduling::READY + || self.status == textsender_models::message::scheduling::PROCESSING + || self.status == textsender_models::message::scheduling::DONE + } else { + false + } + } + } +} + +pub mod response { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct ScheduleMessageResponse { + pub message: String, + pub data: Vec, + } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct GetScheduledMessageResponse { + pub message: String, + pub data: Vec, + } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct CreateScheduleMessageEventResponse { + pub message: String, + pub data: Vec, + } + + pub use CreateScheduleMessageEventResponse as GetScheduledMessageEventResponse; + + pub use CreateScheduleMessageEventResponse as DeleteScheduledMessageEventResponse; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct UpdateScheduledMessageStatusResponse { + pub message: String, + pub data: Vec, + } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct ChangedStatus { + pub old_status: String, + } + + pub use ScheduleMessageResponse as FetchScheduledMessageResponse; +} + +pub mod endpoint { + use crate::repo::contact as contact_repo; + use crate::repo::message as message_repo; + use message_repo::scheduling as scheduling_repo; + + /// Endpoint to create a Scheudled Message + #[utoipa::path( + post, + path = crate::caller::endpoints::SCHEDULE_MESSAGE, + request_body( + content = super::request::ScheduleMessageRequest, + description = "Data needed to create a Scheduled Message", + content_type = "application/json" + ), + responses( + (status = 201, description = "Scheduled Message created", body = super::response::ScheduleMessageResponse), + (status = 400, description = "Error", body = super::response::ScheduleMessageResponse), + (status = 500, description = "Error creating Scheduled Message", body = super::response::ScheduleMessageResponse) + ) +)] + pub async fn schedule_message( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::ScheduleMessageResponse::default(); + + if payload.is_valid() { + if payload.status != textsender_models::message::scheduling::PENDING { + response.message = String::from("scheduled message must be pending"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } else { + if can_schedule(&payload.scheduled) { + let mut scheduled_message = + textsender_models::message::scheduling::ScheduledMessage { + scheduled: payload.scheduled, + status: payload.status, + ..Default::default() + }; + match scheduling_repo::insert(&pool, &scheduled_message, &payload.user_id).await + { + Ok((id, created)) => { + scheduled_message.id = id; + scheduled_message.created = Some(created); + scheduled_message.user_id = payload.user_id; + response.message = String::from("Message scheduled"); + response.data.push(scheduled_message); + (axum::http::StatusCode::CREATED, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Error inserting"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } else { + response.message = String::from("Cannot schedule message"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + + /// Endpoint to get Scheduled Message + #[utoipa::path( + get, + path = crate::caller::endpoints::GET_SCHEDULE_MESSAGE, + params( + ("id" = uuid::Uuid, Path, description = "Id of Scheduled Message"), + ("user_id" = uuid::Uuid, Path, description = "User Id associated with the Scheduled Message") + ), + responses( + (status = 200, description = "Scheduled Message found", body = super::response::GetScheduledMessageResponse), + (status = 400, description = "Error getting Scheduled Message", body = super::response::GetScheduledMessageResponse) + ) + )] + pub async fn get_scheduled_messages( + axum::Extension(pool): axum::Extension, + axum::extract::Query(params): axum::extract::Query< + super::request::GetScheduledMessageParams, + >, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::GetScheduledMessageResponse::default(); + + let scheduled_messages = match params.id { + Some(id) => match scheduling_repo::get(&pool, &id).await { + Ok(scheduled_message) => { + vec![scheduled_message] + } + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => match params.user_id { + Some(user_id) => match scheduling_repo::get_with_user_id(&pool, &user_id).await { + Ok(scheduled_messages) => scheduled_messages, + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => { + response.message = String::from("Invalid parameter"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + }, + }; + + response.data = scheduled_messages; + response.message = String::from(super::super::super::response::SUCCESSFUL); + + (axum::http::StatusCode::OK, axum::Json(response)) + } + + fn can_schedule(scheduled_time: &Option) -> bool { + scheduled_time + .is_some_and(|t| t - time::OffsetDateTime::now_utc() >= time::Duration::minutes(10)) + } + + /// Endpoint to fetch Scheduled Message that is ready + #[utoipa::path( + get, + path = crate::caller::endpoints::FETCH_SCHEDULED_MESSAGE, + responses( + (status = 200, description = "Scheduled Message found", body = super::response::FetchScheduledMessageResponse), + (status = 400, description = "Error getting Scheduled Message", body = super::response::FetchScheduledMessageResponse) + ) + )] + pub async fn fetch_scheduled_message( + axum::Extension(pool): axum::Extension, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::FetchScheduledMessageResponse::default(); + + match scheduling_repo::fetch(&pool).await { + Ok(scheduled_message) => { + response.data.push(scheduled_message); + response.message = String::from(super::super::super::response::SUCCESSFUL); + + (axum::http::StatusCode::OK, axum::Json(response)) + } + Err(err) => match err { + sqlx::Error::RowNotFound => { + response.message = String::from("Nothing to fetch"); + + (axum::http::StatusCode::NO_CONTENT, axum::Json(response)) + } + _ => { + eprintln!("Error: {err:?}"); + response.message = String::from("Error"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + }, + } + } + + /// Endpoint to update status of Scheduled Message + #[utoipa::path( + patch, + path = crate::caller::endpoints::UPDATE_SCHEDULED_MESSAGE_STATUS, + request_body( + content = super::request::UpdateScheduledMessageStatusRequest, + description = "Data needed to update status of Scheduled Message", + content_type = "application/json" + ), + responses( + (status = 200, description = "Status updated", body = super::response::UpdateScheduledMessageStatusResponse), + (status = 400, description = "Error", body = super::response::UpdateScheduledMessageStatusResponse), + (status = 500, description = "Error updating", body = super::response::UpdateScheduledMessageStatusResponse) + ) +)] + pub async fn update_scheduled_message_status( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::UpdateScheduledMessageStatusResponse::default(); + + if payload.is_valid() { + match scheduling_repo::get(&pool, &payload.scheduled_message_id).await { + Ok(scheduled_message) => { + let old_status = scheduled_message.status; + if old_status == payload.status { + response.message = String::from("No change"); + (axum::http::StatusCode::NOT_MODIFIED, axum::Json(response)) + } else { + match scheduling_repo::update_status( + &pool, + &scheduled_message.id, + &payload.status, + ) + .await + { + Ok(_) => { + response.message = + String::from(super::super::super::response::SUCCESSFUL); + response.data = vec![super::response::ChangedStatus { old_status }]; + (axum::http::StatusCode::OK, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Invalid"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Invalid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + + /// Endpoint to create a Scheudled Message Event + #[utoipa::path( + post, + path = crate::caller::endpoints::CREATE_SCHEDULED_MESSAGE_EVENT, + request_body( + content = super::request::CreateScheduleMessageEventRequest, + description = "Data needed to create a Scheduled Message Event", + content_type = "application/json" + ), + responses( + (status = 201, description = "Scheduled Message Event created", body = super::response::CreateScheduleMessageEventResponse), + (status = 400, description = "Error", body = super::response::CreateScheduleMessageEventResponse), + (status = 500, description = "Error creating Scheduled Message Event", body = super::response::CreateScheduleMessageEventResponse) + ) +)] + pub async fn create_scheduled_message_event( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::CreateScheduleMessageEventResponse::default(); + + if payload.is_valid() { + match scheduling_repo::get(&pool, &payload.scheduled_message_id).await { + Ok(_) => {} + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Scheduled Message does not exist"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + } + + match contact_repo::get(&pool, &payload.contact_id).await { + Ok(_) => {} + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Contact does not exist"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + } + + match message_repo::get(&pool, &payload.message_id).await { + Ok(_) => {} + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Contact does not exist"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + } + + let mut scme = textsender_models::message::scheduling::ScheduledMessageEvent { + contact_id: payload.contact_id, + message_id: payload.message_id, + scheduled_message_id: payload.scheduled_message_id, + ..Default::default() + }; + match scheduling_repo::sched_msg_event::insert(&pool, &scme).await { + Ok((id, created)) => { + scme.id = id; + scme.created = Some(created); + response.message = String::from(super::super::super::response::SUCCESSFUL); + response.data.push(scme); + (axum::http::StatusCode::CREATED, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Error inserting"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + + /// Endpoint to get Scheduled Message Event + #[utoipa::path( + get, + path = crate::caller::endpoints::GET_SCHEDULED_MESSAGE_EVENT, + params( + ("id" = uuid::Uuid, Path, description = "Id of Scheduled Message Event"), + ("scheduled_message_id" = uuid::Uuid, Path, description = "Scheduled Message Id associated with the Scheduled Message Event") + ), + responses( + (status = 200, description = "Scheduled Message Event found", body = super::response::GetScheduledMessageEventResponse), + (status = 400, description = "Invalid request", body = super::response::GetScheduledMessageEventResponse), + (status = 500, description = "Error getting Scheduled Message Event", body = super::response::GetScheduledMessageEventResponse) + ) + )] + pub async fn get_scheduled_message_events( + axum::Extension(pool): axum::Extension, + axum::extract::Query(params): axum::extract::Query< + super::request::GetScheduledMessageEventParams, + >, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::GetScheduledMessageEventResponse::default(); + + let scheduled_message_events = match params.id { + Some(id) => match scheduling_repo::sched_msg_event::get(&pool, &id).await { + Ok(scheduled_message) => { + vec![scheduled_message] + } + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => match params.scheduled_message_id { + Some(scheduled_message_id) => { + match scheduling_repo::sched_msg_event::get_with_scheduled_message_id( + &pool, + &scheduled_message_id, + ) + .await + { + Ok(scheduled_messages) => scheduled_messages, + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + } + } + None => { + response.message = String::from("Invalid parameter"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + }, + }; + + response.data = scheduled_message_events; + response.message = String::from(super::super::super::response::SUCCESSFUL); + + (axum::http::StatusCode::OK, axum::Json(response)) + } + + /// Endpoint to delete the Scheduled Message Event + #[utoipa::path( + delete, + path = crate::caller::endpoints::DELETE_SCHEDULED_MESSAGE_EVENT, + params(("id" = uuid::Uuid, Path, description = "Scheduled Message Event Id")), + responses( + (status = 200, description = "Deleted", body = super::response::DeleteScheduledMessageEventResponse), + (status = 400, description = "Bad request", body = super::response::DeleteScheduledMessageEventResponse), + (status = 500, description = "Error deleting", body = super::response::DeleteScheduledMessageEventResponse) + ) + )] + pub async fn delete_scheduled_message_event( + axum::Extension(pool): axum::Extension, + axum::extract::Path(id): axum::extract::Path, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::DeleteScheduledMessageEventResponse::default(); + match scheduling_repo::sched_msg_event::get(&pool, &id).await { + Ok(scheduled_message_event) => { + match scheduling_repo::sched_msg_event::delete(&pool, &scheduled_message_event) + .await + { + Ok(_) => { + response.message = String::from(super::super::super::response::SUCCESSFUL); + response.data.push(scheduled_message_event); + (axum::http::StatusCode::OK, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Error deleting"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Bad request"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + } +} diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 17bc20a..8b747d6 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -1,4 +1,6 @@ pub mod contact; +pub mod instant; +pub mod message; pub mod endpoints { pub const ROOT: &str = "/"; @@ -9,6 +11,31 @@ pub mod endpoints { pub const GET_CONTACT: &str = "/api/v1/contact"; /// Constant for updating names of a Contact endpoint pub const UPDATE_CONTACT_NAME: &str = "/api/v1/contact/update"; + /// Constant for adding message endpoint + pub const ADD_MESSAGE: &str = "/api/v1/message/new"; + /// Constant for getting messages endpoint + pub const GET_MESSAGE: &str = "/api/v1/message"; + /// Constant for scheduling message endpoint + pub const SCHEDULE_MESSAGE: &str = "/api/v1/schedule/message"; + /// Constant for getting scheduled message endpoint + pub const GET_SCHEDULE_MESSAGE: &str = "/api/v1/schedule/message"; + /// Constant for updating scheduled message status endpoint + pub const UPDATE_SCHEDULED_MESSAGE_STATUS: &str = "/api/v1/schedule/message/status/update"; + /// Constant for creating Scheduled Message Event endpoint + pub const CREATE_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event"; + /// Constant for getting Scheduled Message Event endpoint + pub const GET_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event"; + /// Constant for deleting Scheduled Message Event endpoint + pub const DELETE_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event/{id}"; + /// Constant for fetching Scheduled Message endpoint + pub const FETCH_SCHEDULED_MESSAGE: &str = "/api/v1/schedule/message/fetch"; + /// Constant for recording Message Event Response endpoint + pub const RECORD_MESSAGE_EVENT_RESPONSE: &str = + "/api/v1/schedule/message/event/response/record"; + /// Constant for getting Message Event Response endpoint + pub const GET_MESSAGE_EVENT_RESPONSE: &str = "/api/v1/schedule/message/event/response"; + /// Constant for sending a message instantly. No scheduling + pub const INSTANT_MESSAGE: &str = "/api/v1/instant/message"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 105fb1c..a52ebb8 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,13 +10,23 @@ pub mod host { pub mod init { use std::time::Duration; - use axum::routing::{get, patch, post}; + use axum::routing::{delete, get, patch, post}; use tower_http::timeout::TimeoutLayer; use utoipa::OpenApi; use crate::caller::contact as contact_caller; + use crate::caller::message as message_caller; + use crate::caller::message::event as event_caller; + use crate::caller::message::scheduling as scheduling_caller; + use contact_caller::endpoint as contact_endpoints; - // use contact_caller::response as contact_responses; + use contact_caller::response as contact_responses; + use event_caller::endpoint as event_endpoints; + use event_caller::response as event_responses; + use message_caller::endpoint as message_endpoints; + use message_caller::response as message_responses; + use scheduling_caller::endpoint as scheduling_endpoints; + use scheduling_caller::response as scheduling_responses; mod cors { pub async fn configure_cors() -> tower_http::cors::CorsLayer { @@ -73,8 +83,14 @@ pub mod init { #[derive(utoipa::OpenApi)] #[openapi( - paths(contact_endpoints::create_contact,), - components(schemas(contact_caller::response::AddContactResponse)), + paths(contact_endpoints::create_contact, + message_endpoints::create_message, + scheduling_endpoints::schedule_message, + ), + components(schemas(contact_responses::AddContactResponse, message_responses::AddMessageResponse, + scheduling_responses::ScheduleMessageResponse, + event_responses::RecordMessageEventResponse + )), tags( (name = "textsender API", description = "Web API to manage texting") ) @@ -101,6 +117,78 @@ pub mod init { axum::middleware::from_fn(crate::auth::auth::), ), ) + .route( + crate::caller::endpoints::ADD_MESSAGE, + post(message_endpoints::create_message).route_layer(axum::middleware::from_fn( + crate::auth::auth::, + )), + ) + .route( + crate::caller::endpoints::GET_MESSAGE, + get(message_endpoints::get_messages).route_layer(axum::middleware::from_fn( + crate::auth::auth::, + )), + ) + .route( + crate::caller::endpoints::SCHEDULE_MESSAGE, + post(scheduling_endpoints::schedule_message).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::GET_SCHEDULE_MESSAGE, + get(scheduling_endpoints::get_scheduled_messages).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::CREATE_SCHEDULED_MESSAGE_EVENT, + post(scheduling_endpoints::create_scheduled_message_event).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::GET_SCHEDULED_MESSAGE_EVENT, + get(scheduling_endpoints::get_scheduled_message_events).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::DELETE_SCHEDULED_MESSAGE_EVENT, + delete(scheduling_endpoints::delete_scheduled_message_event).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::UPDATE_SCHEDULED_MESSAGE_STATUS, + patch(scheduling_endpoints::update_scheduled_message_status).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::FETCH_SCHEDULED_MESSAGE, + get(scheduling_endpoints::fetch_scheduled_message).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::RECORD_MESSAGE_EVENT_RESPONSE, + post(event_endpoints::record_message_event_response).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::GET_MESSAGE_EVENT_RESPONSE, + get(event_endpoints::get_record_message_event_response).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) + .route( + crate::caller::endpoints::INSTANT_MESSAGE, + post(crate::caller::instant::endpoint::send_message).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message/event.rs b/src/repo/message/event.rs new file mode 100644 index 0000000..31eb864 --- /dev/null +++ b/src/repo/message/event.rs @@ -0,0 +1,112 @@ +use sqlx::Row; + +pub async fn insert( + pool: &sqlx::PgPool, + event: &textsender_models::message::event::MessageEventResponse, +) -> Result { + match sqlx::query( + r#" + INSERT INTO "message_event_responses" (scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status) + VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING id; + "#, + ) + .bind(event.scheduled_message_event_id) + .bind(&event.response) + .bind(event.user_id) + .bind(event.sent) + .bind(event.contact_id) + .bind(event.message_id) + .bind(&event.status) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + Ok(id) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get( + pool: &sqlx::PgPool, + id: &uuid::Uuid, +) -> Result { + match sqlx::query( + r#" + SELECT id, scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status FROM "message_event_responses" + WHERE + id = $1; + "#, + ) + .bind(id) + .fetch_one(pool) + .await + { + Ok(row) => match parse_row(&row).await { + Ok(event) => Ok(event), + Err(err) => Err(err), + }, + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get_with_user_id( + pool: &sqlx::PgPool, + user_id: &uuid::Uuid, +) -> Result, sqlx::Error> { + match sqlx::query( + r#" + SELECT id, scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status FROM "message_event_responses" + WHERE + user_id = $1; + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await + { + Ok(rows) => { + let mut events: Vec = + Vec::new(); + for row in rows { + match parse_row(&row).await { + Ok(event) => { + events.push(event); + } + Err(err) => { + return Err(err); + } + } + } + + Ok(events) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +async fn parse_row( + row: &sqlx::postgres::PgRow, +) -> Result { + println!("Parsing MER"); + let id: uuid::Uuid = row.try_get("id")?; + let response: serde_json::Value = row.try_get("response")?; + let status: String = row.try_get("status")?; + let contact_id: uuid::Uuid = row.try_get("contact_id")?; + let message_id: uuid::Uuid = row.try_get("message_id")?; + let sent: time::OffsetDateTime = row.try_get("sent")?; + let scheduled_message_event_id: uuid::Uuid = row.try_get("scheduled_message_event_id")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + + Ok(textsender_models::message::event::MessageEventResponse { + id, + response, + contact_id, + message_id, + status, + sent: Some(sent), + scheduled_message_event_id: Some(scheduled_message_event_id), + user_id, + }) +} diff --git a/src/repo/message/mod.rs b/src/repo/message/mod.rs new file mode 100644 index 0000000..09b0414 --- /dev/null +++ b/src/repo/message/mod.rs @@ -0,0 +1,94 @@ +pub mod event; +pub mod scheduling; + +use sqlx::Row; + +pub async fn insert( + pool: &sqlx::PgPool, + message: &textsender_models::message::Message, + user_id: &uuid::Uuid, +) -> Result { + match sqlx::query( + r#" + INSERT INTO "messages" (content, user_id) + VALUES($1, $2) RETURNING id; + "#, + ) + .bind(&message.content) + .bind(user_id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + Ok(id) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get( + pool: &sqlx::PgPool, + id: &uuid::Uuid, +) -> Result { + match sqlx::query( + r#" + SELECT id, content, user_id FROM "messages" + WHERE + id = $1; + "#, + ) + .bind(id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + + let content: String = row.try_get("content")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + Ok(textsender_models::message::Message { + id: Some(id), + content, + user_id: Some(user_id), + }) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get_with_user_id( + pool: &sqlx::PgPool, + user_id: &uuid::Uuid, +) -> Result, sqlx::Error> { + match sqlx::query( + r#" + SELECT id, content, user_id FROM "messages" + WHERE + user_id = $1; + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await + { + Ok(rows) => { + let mut messages: Vec = Vec::new(); + for row in rows { + let id: uuid::Uuid = row.try_get("id")?; + let content: String = row.try_get("content")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + + let message = textsender_models::message::Message { + id: Some(id), + content, + user_id: Some(user_id), + }; + messages.push(message); + } + + Ok(messages) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} diff --git a/src/repo/message/scheduling.rs b/src/repo/message/scheduling.rs new file mode 100644 index 0000000..1604350 --- /dev/null +++ b/src/repo/message/scheduling.rs @@ -0,0 +1,280 @@ +use sqlx::Row; + +pub async fn insert( + pool: &sqlx::PgPool, + scheduled_message: &textsender_models::message::scheduling::ScheduledMessage, + user_id: &uuid::Uuid, +) -> Result<(uuid::Uuid, time::OffsetDateTime), sqlx::Error> { + match sqlx::query( + r#" + INSERT INTO "scheduled_messages" (scheduled, status, user_id) + VALUES($1, $2, $3) RETURNING id, created; + "#, + ) + .bind(scheduled_message.scheduled) + .bind(&scheduled_message.status) + .bind(user_id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + let created: time::OffsetDateTime = row.try_get("created")?; + Ok((id, created)) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get( + pool: &sqlx::PgPool, + id: &uuid::Uuid, +) -> Result { + match sqlx::query( + r#" + SELECT id, scheduled, created, status, user_id FROM "scheduled_messages" + WHERE + id = $1; + "#, + ) + .bind(id) + .fetch_one(pool) + .await + { + Ok(row) => match parse_row(&row).await { + Ok(scheduled_message) => Ok(scheduled_message), + Err(err) => Err(err), + }, + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn get_with_user_id( + pool: &sqlx::PgPool, + user_id: &uuid::Uuid, +) -> Result, sqlx::Error> { + match sqlx::query( + r#" + SELECT id, scheduled, created, status, user_id FROM "scheduled_messages" + WHERE + user_id = $1; + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await + { + Ok(rows) => { + let mut messages: Vec = + Vec::new(); + for row in rows { + match parse_row(&row).await { + Ok(scheduled_message) => { + messages.push(scheduled_message); + } + Err(err) => { + return Err(err); + } + } + } + + Ok(messages) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } +} + +pub async fn fetch( + pool: &sqlx::PgPool, +) -> Result { + match sqlx::query( + r#" + UPDATE "scheduled_messages" + SET status = $1 + WHERE id = ( + SELECT id FROM "scheduled_messages" + WHERE status = $2 + ORDER BY id + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING id, scheduled, created, status, user_id + "#, + ) + .bind(textsender_models::message::scheduling::PROCESSING) + .bind(textsender_models::message::scheduling::READY) + .fetch_one(pool) + .await + { + Ok(row) => match parse_row(&row).await { + Ok(scheduled_message) => Ok(scheduled_message), + Err(err) => Err(err), + }, + Err(err) => Err(err), + } +} + +pub async fn update_status( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + status: &str, +) -> Result<(), sqlx::Error> { + match sqlx::query( + r#" + UPDATE "scheduled_messages" SET status = $1 WHERE id = $2; + "#, + ) + .bind(status) + .bind(id) + .execute(pool) + .await + { + Ok(_row) => Ok(()), + Err(err) => Err(err), + } +} + +async fn parse_row( + row: &sqlx::postgres::PgRow, +) -> Result { + let id: uuid::Uuid = row.try_get("id")?; + let scheduled: time::OffsetDateTime = row.try_get("scheduled")?; + let status: String = row.try_get("status")?; + let created: time::OffsetDateTime = row.try_get("created")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + + Ok(textsender_models::message::scheduling::ScheduledMessage { + id, + scheduled: Some(scheduled), + created: Some(created), + status, + user_id, + }) +} + +pub mod sched_msg_event { + use sqlx::Row; + + pub async fn insert( + pool: &sqlx::PgPool, + scheduled_message_event: &textsender_models::message::scheduling::ScheduledMessageEvent, + ) -> Result<(uuid::Uuid, time::OffsetDateTime), sqlx::Error> { + match sqlx::query( + r#" + INSERT INTO "scheduled_message_events" (contact_id, message_id, scheduled_message_id) + VALUES($1, $2, $3) RETURNING id, created; + "#, + ) + .bind(scheduled_message_event.contact_id) + .bind(scheduled_message_event.message_id) + .bind(scheduled_message_event.scheduled_message_id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + let created: time::OffsetDateTime = row.try_get("created")?; + Ok((id, created)) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } + } + + pub async fn get( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + ) -> Result { + match sqlx::query( + r#" + SELECT id, contact_id, message_id, scheduled_message_id, created FROM "scheduled_message_events" + WHERE + id = $1; + "#, + ) + .bind(id) + .fetch_one(pool) + .await + { + Ok(row) => match parse_row(&row).await { + Ok(scheduled_message_event) => Ok(scheduled_message_event), + Err(err) => Err(err), + }, + Err(_) => Err(sqlx::Error::RowNotFound), + } + } + + pub async fn get_with_scheduled_message_id( + pool: &sqlx::PgPool, + scheduled_message_id: &uuid::Uuid, + ) -> Result, sqlx::Error> + { + match sqlx::query( + r#" + SELECT id, contact_id, message_id, scheduled_message_id, created FROM "scheduled_message_events" + WHERE + scheduled_message_id = $1; + "#, + ) + .bind(scheduled_message_id) + .fetch_all(pool) + .await + { + Ok(rows) => { + let mut scheduled_message_events: Vec = + Vec::new(); + for row in rows { + match parse_row(&row).await { + Ok(scheduled_message_event) => { + scheduled_message_events.push(scheduled_message_event); + } + Err(err) => { + return Err(err); + } + } + } + + Ok(scheduled_message_events) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } + } + + pub async fn delete( + pool: &sqlx::PgPool, + scheduled_message_event: &textsender_models::message::scheduling::ScheduledMessageEvent, + ) -> Result<(), sqlx::Error> { + match sqlx::query( + r#" + DELETE FROM "scheduled_message_events" + WHERE id = $1 + "#, + ) + .bind(scheduled_message_event.id) + .execute(pool) + .await + { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } + + async fn parse_row( + row: &sqlx::postgres::PgRow, + ) -> Result { + let id: uuid::Uuid = row.try_get("id")?; + let contact_id: uuid::Uuid = row.try_get("contact_id")?; + let message_id: uuid::Uuid = row.try_get("message_id")?; + let scheduled_message_id: uuid::Uuid = row.try_get("scheduled_message_id")?; + let created: time::OffsetDateTime = row.try_get("created")?; + + Ok( + textsender_models::message::scheduling::ScheduledMessageEvent { + id, + contact_id, + message_id, + scheduled_message_id, + created: Some(created), + }, + ) + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 40d6353..2af9758 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1 +1,2 @@ pub mod contact; +pub mod message; diff --git a/tests/test.rs b/tests/test.rs index 801fc62..09c8172 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,8 +1,3 @@ -// use std::io::Write; - -// use common_multipart_rfc7578::client::multipart::{Body as MultipartBody, Form as MultipartForm}; -// use tower::ServiceExt; - use textsender_api::db; mod db_mgr { @@ -36,20 +31,52 @@ mod db_mgr { template_pool: &sqlx::PgPool, db_name: &str, ) -> Result<(), sqlx::Error> { - let create_query = format!("CREATE DATABASE {}", db_name); - match sqlx::query(&create_query).execute(template_pool).await { + match sqlx::query(sqlx::AssertSqlSafe(format!( + r#"CREATE DATABASE {}"#, + db_name + ))) + .execute(template_pool) + .await + { Ok(_) => Ok(()), Err(e) => Err(e), } } + pub async fn get_database_ready() -> ( + sqlx::Pool, + String, + sqlx::Pool, + ) { + let tm_pool = get_pool().await.unwrap(); + let db_name = generate_db_name().await; + + match create_database(&tm_pool, &db_name).await { + Ok(_) => { + println!("Success"); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + } + + let pool = connect_to_db(&db_name).await.unwrap(); + migrations(&pool).await; + + (tm_pool, db_name, pool) + } + // Function to drop a database pub async fn drop_database( template_pool: &sqlx::PgPool, db_name: &str, ) -> Result<(), sqlx::Error> { - let drop_query = format!("DROP DATABASE IF EXISTS {} WITH (FORCE)", db_name); - sqlx::query(&drop_query).execute(template_pool).await?; + sqlx::query(sqlx::AssertSqlSafe(format!( + r#"DROP DATABASE IF EXISTS {} WITH (FORCE)"#, + db_name + ))) + .execute(template_pool) + .await?; Ok(()) } @@ -73,16 +100,9 @@ mod db_mgr { } } - /* pub async fn migrations(pool: &sqlx::PgPool) { - // Run migrations using the sqlx::migrate! macro - // Assumes your test migrations are in a ./test_migrations folder relative to Cargo.toml - sqlx::migrate!("./migrations") - .run(pool) - .await - .expect("Failed to run migrations"); + super::db::migrations(pool).await; } - */ } mod init { @@ -113,12 +133,10 @@ mod util { serde_json::from_slice(&body).unwrap() } - /* pub async fn format_url_with_value(endpoint: &str, value: &uuid::Uuid) -> String { let last = endpoint.len() - 5; format!("{}/{value}", &endpoint[0..last]) } - */ } pub fn token_fields() -> (String, String, String) { @@ -129,8 +147,49 @@ pub fn token_fields() -> (String, String, String) { ) } +/// Test User Id pub const TEST_USER_ID: uuid::Uuid = uuid::uuid!("cc938368-615a-4694-b2ca-6e122fa31c52"); +/// Test contact phone number +pub const TEST_CONTACT_PHONE_NUMBER: &str = "+10123456789"; + +/// Test contact updated firstname +pub const TEST_CONTACT_UPDATED_FIRSTNAME: &str = "Johnny"; +/// Test contact updated lastname +pub const TEST_CONTACT_UPDATED_LASTNAME: &str = "CASH"; +/// Test contact updated nickname +pub const TEST_CONTACT_UPDATED_NICKNAME: &str = "The Man in Black"; + +/// Test message content +pub const TEST_MESSAGE_CONTENT: &str = "The wind cries mary"; + +pub fn test_mer_response() -> serde_json::Value { + serde_json::json!({ + "account_sid": "34n97yf342y5fc349287yndx3987i4yf5c3w98dy398", + "api_version": "2010-04-01", + "body": "HEALINGHURTS.LETITIN.TONIGHT!GOATROPE.TRASHTHERENTAL.THEGHOSTYOUKNOW.SUMOFTHEBROTHERS.10PM.THESTAMP!VERO.LOVEYOURBLOOD.LOVE&LIGHT🙏🔥", + "date_created": "Sat, 20 Jun 2026 15:45:01 +0000", + "date_sent": null, + "date_updated": "Sat, 20 Jun 2026 15:45:01 +0000", + "direction": "outbound-api", + "error_code": null, + "error_message": null, + "from": "+10123456789", + "messaging_service_sid": "329nc29yrc394ytw37yc4t9w37ytnfdxcw39874ydx98347yr", + "num_media": "0", + "num_segments": "0", + "price": null, + "price_unit": null, + "sid": "34niyt3i47ytfdi37w4ydtrn837q4itfyb3qi4ftciq38w7t83qw49td38dtn", + "status": "accepted", + "subresource_uris": { + "media": "/2010-04-01/Accounts/34n97yf342y5fc349287yndx3987i4yf5c3w98dy398/Messages/329nc29yrc394ytw37yc4t9w37ytnfdxcw39874ydx98347yr/Media.json" + }, + "to": "+11234567890", + "uri": "/2010-04-01/Accounts/34n97yf342y5fc349287yndx3987i4yf5c3w98dy398/Messages/329nc29yrc394ytw37yc4t9w37ytnfdxcw39874ydx98347yr.json" + }) +} + pub async fn test_token() -> Result { let key: String = textsender_models::envy::environment::get_secret_main_key() .await @@ -150,6 +209,19 @@ pub async fn test_token() -> Result { } } +pub fn test_valid_schedulable_time() -> time::OffsetDateTime { + let now = time::OffsetDateTime::now_utc(); + now + std::time::Duration::from_secs(60 * 60) +} + +pub fn convert_time_to_iso(time: time::OffsetDateTime) -> Result { + use time::format_description::well_known::Iso8601; + match time.format(&Iso8601::DEFAULT) { + Ok(converted) => Ok(converted), + Err(err) => Err(err), + } +} + pub async fn bearer_auth() -> String { let token = match test_token().await { Ok(access_token) => access_token, @@ -163,54 +235,49 @@ pub async fn bearer_auth() -> String { } mod request { - // use common_multipart_rfc7578::client::multipart::{ - // Body as MultipartBody, Form as MultipartForm, - // }; use tower::ServiceExt; pub async fn create_contact( app: &axum::Router, - ) -> Result { + ) -> Result { let payload = serde_json::json!({ "phone_number": super::TEST_CONTACT_PHONE_NUMBER, "user_id": super::TEST_USER_ID, }); - let req = axum::http::Request::builder() - .method(axum::http::Method::POST) - .uri(textsender_api::caller::endpoints::ADD_CONTACT) - .header(axum::http::header::CONTENT_TYPE, "application/json") - .header( - axum::http::header::AUTHORIZATION, - super::bearer_auth().await, - ) - .body(axum::body::Body::from(payload.to_string())) - .unwrap(); - app.clone().oneshot(req).await + match run_post( + Some(&payload), + textsender_api::caller::endpoints::ADD_CONTACT, + axum::http::Method::POST, + true, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } } pub async fn get_contact( app: &axum::Router, id: &uuid::Uuid, - ) -> Result { + ) -> Result { let uri = format!( "{}?id={}", textsender_api::caller::endpoints::GET_CONTACT, id ); - let req = axum::http::Request::builder() - .method(axum::http::Method::GET) - .uri(uri) - .header(axum::http::header::CONTENT_TYPE, "application/json") - .header( - axum::http::header::AUTHORIZATION, - super::bearer_auth().await, - ) - .body(axum::body::Body::empty()) - .unwrap(); - - app.clone().oneshot(req).await + match run_post(None, &uri, axum::http::Method::GET, false).await { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } } pub async fn update_contact_names( @@ -219,7 +286,7 @@ mod request { firstname: Option<&str>, lastname: Option<&str>, nickname: Option<&str>, - ) -> Result { + ) -> Result { let payload = serde_json::json!({ "firstname": firstname, "lastname": lastname, @@ -228,47 +295,337 @@ mod request { "id": id }); - let req = axum::http::Request::builder() - .method(axum::http::Method::PATCH) - .uri(textsender_api::caller::endpoints::UPDATE_CONTACT_NAME) - .header(axum::http::header::CONTENT_TYPE, "application/json") + match run_post( + Some(&payload), + textsender_api::caller::endpoints::UPDATE_CONTACT_NAME, + axum::http::Method::PATCH, + true, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub mod message { + use tower::ServiceExt; + + pub async fn create_message( + app: &axum::Router, + ) -> Result { + let payload = serde_json::json!({ + "content": super::super::TEST_MESSAGE_CONTENT, + "user_id": super::super::TEST_USER_ID, + }); + + match super::run_post( + Some(&payload), + textsender_api::caller::endpoints::ADD_MESSAGE, + axum::http::Method::POST, + true, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn get_message( + app: &axum::Router, + id: &uuid::Uuid, + ) -> Result { + let uri = format!( + "{}?id={}", + textsender_api::caller::endpoints::GET_MESSAGE, + id + ); + + match super::run_post(None, &uri, axum::http::Method::GET, false).await { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub mod event { + use tower::ServiceExt; + + pub async fn record_message_event_response( + app: &axum::Router, + scheduled_message_event_id: &uuid::Uuid, + user_id: &uuid::Uuid, + contact_id: &uuid::Uuid, + message_id: &uuid::Uuid, + ) -> Result { + let sent = time::OffsetDateTime::now_utc(); + let sent = match super::super::super::convert_time_to_iso(sent) { + Ok(t) => t, + Err(err) => { + assert!(false, "Error: {err:?}"); + String::new() + } + }; + let payload = serde_json::json!({ + "scheduled_message_event_id": scheduled_message_event_id, + "response": super::super::super::test_mer_response(), + "user_id": user_id, + "sent": sent, + "status": textsender_models::message::event::MESSAGE_EVENT_RESPONSE_STATUS_SCHEDULED, + "contact_id": contact_id, + "message_id": message_id + }); + + match super::super::run_post( + Some(&payload), + textsender_api::caller::endpoints::RECORD_MESSAGE_EVENT_RESPONSE, + axum::http::Method::POST, + true, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn get_message_event_response( + app: &axum::Router, + id: &uuid::Uuid, + ) -> Result { + let uri = format!( + "{}?id={}", + textsender_api::caller::endpoints::GET_MESSAGE_EVENT_RESPONSE, + id + ); + + match super::super::run_post(None, &uri, axum::http::Method::GET, false).await { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + } + + pub mod scheduling { + use tower::ServiceExt; + + pub async fn create_scheduled_message( + app: &axum::Router, + ) -> Result { + let scheduled = super::super::super::test_valid_schedulable_time(); + let scheduled = match super::super::super::convert_time_to_iso(scheduled) { + Ok(t) => t, + Err(err) => { + assert!(false, "Error: {err:?}"); + String::new() + } + }; + + let payload = serde_json::json!({ + "scheduled": scheduled, + "status": textsender_models::message::scheduling::PENDING, + "user_id": super::super::super::TEST_USER_ID, + }); + + match super::super::run_post( + Some(&payload), + textsender_api::caller::endpoints::SCHEDULE_MESSAGE, + axum::http::Method::POST, + true, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn get_scheduled_messages( + app: &axum::Router, + id: &uuid::Uuid, + ) -> Result { + let uri = format!( + "{}?id={}", + textsender_api::caller::endpoints::GET_SCHEDULE_MESSAGE, + id + ); + + match super::super::run_post(None, &uri, axum::http::Method::GET, false).await { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn fetch_scheduled_message( + app: &axum::Router, + ) -> Result { + match super::super::run_post( + None, + textsender_api::caller::endpoints::FETCH_SCHEDULED_MESSAGE, + axum::http::Method::GET, + false, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn update_scheduled_message_status( + app: &axum::Router, + id: &uuid::Uuid, + updated_status: &str, + ) -> Result { + let uri = textsender_api::caller::endpoints::UPDATE_SCHEDULED_MESSAGE_STATUS; + let payload = serde_json::json!({ + "scheduled_message_id": id, + "status": updated_status + }); + + match super::super::run_post(Some(&payload), &uri, axum::http::Method::PATCH, true) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn create_scheduled_message_event( + app: &axum::Router, + contact_id: &uuid::Uuid, + message_id: &uuid::Uuid, + scheduled_message_id: &uuid::Uuid, + ) -> Result { + let payload = serde_json::json!({ + "contact_id": contact_id, + "message_id": message_id, + "scheduled_message_id": scheduled_message_id + }); + + println!("Payload: {payload:?}"); + + match super::super::run_post( + Some(&payload), + textsender_api::caller::endpoints::CREATE_SCHEDULED_MESSAGE_EVENT, + axum::http::Method::POST, + true, + ) + .await + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn get_scheduled_message_events( + app: &axum::Router, + scheduled_message_id: &uuid::Uuid, + ) -> Result { + let uri = format!( + "{}?scheduled_message_id={}", + textsender_api::caller::endpoints::GET_SCHEDULED_MESSAGE_EVENT, + scheduled_message_id + ); + + match super::super::run_post(None, &uri, axum::http::Method::GET, false).await { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + + pub async fn delete_scheduled_message_event( + app: &axum::Router, + scheduled_message_event_id: &uuid::Uuid, + ) -> Result { + let uri = super::super::super::util::format_url_with_value( + textsender_api::caller::endpoints::DELETE_SCHEDULED_MESSAGE_EVENT, + scheduled_message_event_id, + ) + .await; + + match super::super::run_post(None, &uri, axum::http::Method::DELETE, false).await { + Ok(req) => match app.clone().oneshot(req).await { + Ok(response) => Ok(response), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } + } + } + + pub async fn run_post( + payload: Option<&serde_json::Value>, + uri: &str, + method: axum::http::Method, + has_body: bool, + ) -> Result, axum::http::Error> { + let body = if has_body { + assert_eq!( + true, + payload.is_some(), + "Has request body and payload has data" + ); + axum::body::Body::from(payload.unwrap().to_string()) + } else { + axum::body::Body::empty() + }; + match axum::http::Request::builder() + .method(method) + .uri(uri) + .header( + axum::http::header::CONTENT_TYPE, + "application/json; charset=utf-8", + ) .header( axum::http::header::AUTHORIZATION, super::bearer_auth().await, ) - .body(axum::body::Body::from(payload.to_string())) - .unwrap(); - app.clone().oneshot(req).await + .body(body) + { + Ok(t) => Ok(t), + Err(err) => Err(err), + } } } -/// Test contact phone number -pub const TEST_CONTACT_PHONE_NUMBER: &str = "+10123456789"; - -/// Test contact updated firstname -pub const TEST_CONTACT_UPDATED_FIRSTNAME: &str = "Johnny"; -/// Test contact updated lastname -pub const TEST_CONTACT_UPDATED_LASTNAME: &str = "CASH"; -/// Test contact updated nickname -pub const TEST_CONTACT_UPDATED_NICKNAME: &str = "The Man in Black"; - #[tokio::test] async fn test_create_contact() { - let tm_pool = db_mgr::get_pool().await.unwrap(); - let db_name = db_mgr::generate_db_name().await; - - match db_mgr::create_database(&tm_pool, &db_name).await { - Ok(_) => { - println!("Success"); - } - Err(err) => { - assert!(false, "Error: {:?}", err); - } - } - - let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); - db::migrations(&pool).await; - + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; let app = init::app(pool).await; // Send request @@ -290,21 +647,7 @@ async fn test_create_contact() { #[tokio::test] async fn test_get_contact() { - let tm_pool = db_mgr::get_pool().await.unwrap(); - let db_name = db_mgr::generate_db_name().await; - - match db_mgr::create_database(&tm_pool, &db_name).await { - Ok(_) => { - println!("Success"); - } - Err(err) => { - assert!(false, "Error: {:?}", err); - } - } - - let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); - db::migrations(&pool).await; - + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; let app = init::app(pool).await; // Send request @@ -345,21 +688,7 @@ async fn test_get_contact() { #[tokio::test] async fn test_update_contact_names() { - let tm_pool = db_mgr::get_pool().await.unwrap(); - let db_name = db_mgr::generate_db_name().await; - - match db_mgr::create_database(&tm_pool, &db_name).await { - Ok(_) => { - println!("Success"); - } - Err(err) => { - assert!(false, "Error: {:?}", err); - } - } - - let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); - db::migrations(&pool).await; - + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; let app = init::app(pool).await; let mut opt_contact: Option = None; @@ -433,3 +762,900 @@ async fn test_update_contact_names() { let _ = db_mgr::drop_database(&tm_pool, &db_name).await; } + +#[tokio::test] +async fn test_create_message() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + // Send request + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + assert_eq!( + TEST_MESSAGE_CONTENT, message.content, + "The content of the created message should match" + ); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_get_message() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + let mut message_result: Option = None; + + // Send request + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + assert_eq!( + TEST_MESSAGE_CONTENT, message.content, + "The content of the created message should match" + ); + message_result = Some(message.clone()); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + assert_eq!(true, message_result.is_some()); + + let message = message_result.unwrap(); + let message_id = message.id.unwrap(); + match request::message::get_message(&app, &message_id).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::GetMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_schedule_message() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_get_scheduled_message() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut id: Option = None; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let scheduled_message = &resp.data[0]; + id = Some(scheduled_message.id); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + if let Some(parsed_id) = id { + match request::message::scheduling::get_scheduled_messages(&app, &parsed_id).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::GetScheduledMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } else { + assert!(false, "Scheduled Message Id could not be retrieved"); + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_create_scheduled_message_event() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_get_scheduled_message_event() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::get_scheduled_message_events(&app, &scheduled_message_id) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::GetScheduledMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_delete_scheduled_message_event() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::get_scheduled_message_events(&app, &scheduled_message_id) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::GetScheduledMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let scheduled_message_event = &resp.data[0]; + match request::message::scheduling::delete_scheduled_message_event( + &app, + &scheduled_message_event.id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::DeleteScheduledMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_update_scheduled_message_status() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::update_scheduled_message_status( + &app, + &scheduled_message_id, + textsender_models::message::scheduling::READY, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::UpdateScheduledMessageStatusResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_fetch_scheduled_message() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::update_scheduled_message_status( + &app, + &scheduled_message_id, + textsender_models::message::scheduling::READY, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::UpdateScheduledMessageStatusResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::fetch_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::FetchScheduledMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_record_message_event_response() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_event_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let scheduled_message_event = &resp.data[0]; + scheduled_message_event_id = scheduled_message_event.id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::update_scheduled_message_status( + &app, + &scheduled_message_id, + textsender_models::message::scheduling::READY, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::UpdateScheduledMessageStatusResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::fetch_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::FetchScheduledMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + assert_eq!( + false, + scheduled_message_event_id.is_nil(), + "Scheduled Message Event Id should not be empty" + ); + + match request::message::event::record_message_event_response( + &app, + &scheduled_message_event_id, + &TEST_USER_ID, + &contact_id, + &message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::event::response::RecordMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +} + +#[tokio::test] +async fn test_get_message_event_response() { + let (tm_pool, db_name, pool) = db_mgr::get_database_ready().await; + let app = init::app(pool).await; + + let mut contact_id: uuid::Uuid = uuid::Uuid::nil(); + let mut message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_id: uuid::Uuid = uuid::Uuid::nil(); + let mut scheduled_message_event_id: uuid::Uuid = uuid::Uuid::nil(); + + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + contact_id = resp.data[0].id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::create_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::response::AddMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let message = &resp.data[0]; + message_id = message.id.unwrap(); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + match request::message::scheduling::create_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + scheduled_message_id = resp.data[0].id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::create_scheduled_message_event( + &app, + &contact_id, + &message_id, + &scheduled_message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::CreateScheduleMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let scheduled_message_event = &resp.data[0]; + scheduled_message_event_id = scheduled_message_event.id; + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::update_scheduled_message_status( + &app, + &scheduled_message_id, + textsender_models::message::scheduling::READY, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::UpdateScheduledMessageStatusResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match request::message::scheduling::fetch_scheduled_message(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::scheduling::response::FetchScheduledMessageResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + assert_eq!( + false, + scheduled_message_event_id.is_nil(), + "Scheduled Message Event Id should not be empty" + ); + + match request::message::event::record_message_event_response( + &app, + &scheduled_message_event_id, + &TEST_USER_ID, + &contact_id, + &message_id, + ) + .await + { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::event::response::RecordMessageEventResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let mer = &resp.data[0]; + match request::message::event::get_message_event_response(&app, &mer.id).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::message::event::response::GetMERResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +}