From d86958035e048c42088668a5c13d06fe94568cdc Mon Sep 17 00:00:00 2001 From: phoenix Date: Wed, 17 Jun 2026 14:25:53 -0400 Subject: [PATCH 01/20] Add message endpoint (#13) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/13 --- Cargo.lock | 2 +- Cargo.toml | 2 +- migrations/20260613211440_init.sql | 6 +++ migrations/schema.sql | 49 ----------------- src/caller/message.rs | 84 ++++++++++++++++++++++++++++++ src/caller/mod.rs | 3 ++ src/config/mod.rs | 16 ++++-- src/repo/message.rs | 25 +++++++++ src/repo/mod.rs | 1 + 9 files changed, 134 insertions(+), 54 deletions(-) delete mode 100644 migrations/schema.sql create mode 100644 src/caller/message.rs create mode 100644 src/repo/message.rs diff --git a/Cargo.lock b/Cargo.lock index dc3eaa9..29d1fdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,7 +2293,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.7" +version = "0.1.8" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index ccf8ec5..5797080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.7" +version = "0.1.8" edition = "2024" rust-version = "1.96" diff --git a/migrations/20260613211440_init.sql b/migrations/20260613211440_init.sql index ef5cc2b..caf9db4 100644 --- a/migrations/20260613211440_init.sql +++ b/migrations/20260613211440_init.sql @@ -10,3 +10,9 @@ 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 +); 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/message.rs b/src/caller/message.rs new file mode 100644 index 0000000..05d8280 --- /dev/null +++ b/src/caller/message.rs @@ -0,0 +1,84 @@ +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() + } + } +} + +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 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)) + } + } +} diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 17bc20a..85b102e 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -1,4 +1,5 @@ pub mod contact; +pub mod message; pub mod endpoints { pub const ROOT: &str = "/"; @@ -9,6 +10,8 @@ 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 105fb1c..f8a03e9 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,8 +15,11 @@ pub mod init { use utoipa::OpenApi; use crate::caller::contact as contact_caller; + use crate::caller::message as message_caller; use contact_caller::endpoint as contact_endpoints; - // use contact_caller::response as contact_responses; + use contact_caller::response as contact_responses; + use message_caller::endpoint as message_endpoints; + use message_caller::response as message_responses; mod cors { pub async fn configure_cors() -> tower_http::cors::CorsLayer { @@ -73,8 +76,9 @@ 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), + components(schemas(contact_responses::AddContactResponse, message_responses::AddMessageResponse)), tags( (name = "textsender API", description = "Web API to manage texting") ) @@ -101,6 +105,12 @@ 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::, + )), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message.rs b/src/repo/message.rs new file mode 100644 index 0000000..312e893 --- /dev/null +++ b/src/repo/message.rs @@ -0,0 +1,25 @@ +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), + } +} 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; From 5740bce97c691f07118ccf8901335424ef797d0e Mon Sep 17 00:00:00 2001 From: phoenix Date: Wed, 17 Jun 2026 15:54:26 -0400 Subject: [PATCH 02/20] Get message endpoint (#14) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/14 --- .gitea/workflows/tests.yml | 91 +++++++++++++ Cargo.lock | 5 +- Cargo.toml | 5 +- src/caller/contact.rs | 2 +- src/caller/message.rs | 67 ++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 6 + src/repo/message.rs | 66 +++++++++ tests/test.rs | 268 +++++++++++++++++++++++++++++++------ 9 files changed, 461 insertions(+), 51 deletions(-) create mode 100644 .gitea/workflows/tests.yml diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..5d64798 --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,91 @@ +name: Rust Build + +on: + pull_request: + branches: + - alot_of_changes + + +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.3-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/Cargo.lock b/Cargo.lock index 29d1fdd..993ac87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,22 +2293,19 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.8" +version = "0.1.9" dependencies = [ "axum", "axum-extra", - "base64", "common-multipart-rfc7578", "futures", "josekit", "jsonwebtoken", - "mime_guess", "serde", "serde_json", "sqlx", "tempfile", "textsender_models", - "thiserror 2.0.18", "time", "tokio", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index 5797080..44b5dc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.8" +version = "0.1.9" edition = "2024" rust-version = "1.96" @@ -15,12 +15,9 @@ 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"] } 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"] } 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/message.rs b/src/caller/message.rs index 05d8280..3e07c0a 100644 --- a/src/caller/message.rs +++ b/src/caller/message.rs @@ -13,6 +13,12 @@ pub mod request { !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 { @@ -24,6 +30,8 @@ pub mod response { pub message: String, pub data: Vec, } + + pub use AddMessageResponse as GetMessageResponse; } pub mod endpoint { @@ -81,4 +89,63 @@ pub mod endpoint { (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/mod.rs b/src/caller/mod.rs index 85b102e..3a9e62a 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -12,6 +12,8 @@ pub mod endpoints { 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index f8a03e9..111e1b0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -111,6 +111,12 @@ pub mod init { crate::auth::auth::, )), ) + .route( + crate::caller::endpoints::GET_MESSAGE, + get(message_endpoints::get_messages).route_layer(axum::middleware::from_fn( + crate::auth::auth::, + )), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message.rs b/src/repo/message.rs index 312e893..0e3b015 100644 --- a/src/repo/message.rs +++ b/src/repo/message.rs @@ -23,3 +23,69 @@ pub async fn insert( 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/tests/test.rs b/tests/test.rs index 801fc62..6bb0b09 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -129,8 +129,22 @@ 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 async fn test_token() -> Result { let key: String = textsender_models::envy::environment::get_secret_main_key() .await @@ -170,47 +184,45 @@ mod request { 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 +231,7 @@ mod request { firstname: Option<&str>, lastname: Option<&str>, nickname: Option<&str>, - ) -> Result { + ) -> Result { let payload = serde_json::json!({ "firstname": firstname, "lastname": lastname, @@ -228,30 +240,101 @@ mod request { "id": id }); - let req = axum::http::Request::builder() - .method(axum::http::Method::PATCH) - .uri(textsender_api::caller::endpoints::UPDATE_CONTACT_NAME) + 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 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") .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(); @@ -433,3 +516,104 @@ 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_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 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_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 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; +} From d1bc5eb830148c12a4a5ee4160ed62d0d7ffa899 Mon Sep 17 00:00:00 2001 From: phoenix Date: Wed, 17 Jun 2026 17:51:33 -0400 Subject: [PATCH 03/20] Schedule message (#15) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/15 --- Cargo.lock | 6 +- Cargo.toml | 4 +- migrations/20260613211440_init.sql | 8 + src/caller/{message.rs => message/mod.rs} | 2 + src/caller/message/scheduling.rs | 104 +++++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 17 ++- src/repo/mod.rs | 1 + src/repo/scheduling.rs | 27 ++++ tests/test.rs | 177 +++++++++++----------- 10 files changed, 256 insertions(+), 92 deletions(-) rename src/caller/{message.rs => message/mod.rs} (99%) create mode 100644 src/caller/message/scheduling.rs create mode 100644 src/repo/scheduling.rs diff --git a/Cargo.lock b/Cargo.lock index 993ac87..3989e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,7 +2293,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.9" +version = "0.1.10" dependencies = [ "axum", "axum-extra", @@ -2320,8 +2320,8 @@ 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.1" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.1-29-ad632fca16-111#ad632fca168ec817e61dd364716f8030c9cfb0b3" dependencies = [ "const_format", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 44b5dc4..c5beac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.9" +version = "0.1.10" edition = "2024" rust-version = "1.96" @@ -22,7 +22,7 @@ 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.1-29-ad632fca16-111" } [dev-dependencies] common-multipart-rfc7578 = { version = "0.7.0" } diff --git a/migrations/20260613211440_init.sql b/migrations/20260613211440_init.sql index caf9db4..48d8e47 100644 --- a/migrations/20260613211440_init.sql +++ b/migrations/20260613211440_init.sql @@ -16,3 +16,11 @@ CREATE TABLE IF NOT EXISTS "messages" ( 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 +); diff --git a/src/caller/message.rs b/src/caller/message/mod.rs similarity index 99% rename from src/caller/message.rs rename to src/caller/message/mod.rs index 3e07c0a..3d28da6 100644 --- a/src/caller/message.rs +++ b/src/caller/message/mod.rs @@ -1,3 +1,5 @@ +pub mod scheduling; + pub mod request { use serde::{Deserialize, Serialize}; use utoipa::ToSchema; diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs new file mode 100644 index 0000000..de9f85e --- /dev/null +++ b/src/caller/message/scheduling.rs @@ -0,0 +1,104 @@ +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() + } + } +} + +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, + } +} + +pub mod endpoint { + use crate::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)) + } + } + + fn can_schedule(scheduled_time: &Option) -> bool { + scheduled_time + .is_some_and(|t| t - time::OffsetDateTime::now_utc() >= time::Duration::minutes(10)) + } +} diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 3a9e62a..d436f86 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -14,6 +14,8 @@ pub mod endpoints { 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 111e1b0..7199ff2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -16,10 +16,14 @@ pub mod init { use crate::caller::contact as contact_caller; use crate::caller::message as message_caller; + use crate::caller::message::scheduling as scheduling_caller; + use contact_caller::endpoint as contact_endpoints; use contact_caller::response as contact_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 { @@ -77,8 +81,11 @@ pub mod init { #[derive(utoipa::OpenApi)] #[openapi( paths(contact_endpoints::create_contact, - message_endpoints::create_message), - components(schemas(contact_responses::AddContactResponse, message_responses::AddMessageResponse)), + message_endpoints::create_message, + scheduling_endpoints::schedule_message, + ), + components(schemas(contact_responses::AddContactResponse, message_responses::AddMessageResponse, + scheduling_responses::ScheduleMessageResponse)), tags( (name = "textsender API", description = "Web API to manage texting") ) @@ -117,6 +124,12 @@ pub mod init { crate::auth::auth::, )), ) + .route( + crate::caller::endpoints::SCHEDULE_MESSAGE, + post(scheduling_endpoints::schedule_message).route_layer( + axum::middleware::from_fn(crate::auth::auth::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 2af9758..d185221 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,2 +1,3 @@ pub mod contact; pub mod message; +pub mod scheduling; diff --git a/src/repo/scheduling.rs b/src/repo/scheduling.rs new file mode 100644 index 0000000..b03e93c --- /dev/null +++ b/src/repo/scheduling.rs @@ -0,0 +1,27 @@ +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), + } +} diff --git a/tests/test.rs b/tests/test.rs index 6bb0b09..90e8749 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,8 +1,6 @@ // 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 { @@ -43,6 +41,29 @@ mod db_mgr { } } + 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, @@ -73,16 +94,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 { @@ -301,6 +315,46 @@ mod request { Err(err) => Err(err), } } + + pub mod scheduling { + use time::format_description::well_known::Iso8601; + use tower::ServiceExt; + + pub async fn create_scheduled_message( + app: &axum::Router, + ) -> Result { + let now = time::OffsetDateTime::now_utc(); + let scheduled = now + std::time::Duration::from_secs(60 * 60); + let scheduled = match scheduled.format(&Iso8601::DEFAULT) { + 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 run_post( @@ -337,21 +391,7 @@ mod request { #[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 @@ -373,21 +413,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 @@ -428,21 +454,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; @@ -519,21 +531,7 @@ async fn test_update_contact_names() { #[tokio::test] async fn test_create_message() { - 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 @@ -560,21 +558,7 @@ async fn test_create_message() { #[tokio::test] async fn test_get_message() { - 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 message_result: Option = None; @@ -617,3 +601,26 @@ async fn test_get_message() { 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) => { + eprintln!("Response: {response:?}"); + println!("Response: {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; +} From 825575930e7d5f09e14ee3b20e0e30a368da8114 Mon Sep 17 00:00:00 2001 From: phoenix Date: Thu, 18 Jun 2026 14:19:21 -0400 Subject: [PATCH 04/20] Get schedule message endpoint (#16) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/16 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/caller/message/scheduling.rs | 73 ++++++++++++++++++++++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 6 +++ src/repo/scheduling.rs | 76 ++++++++++++++++++++++++++++++++ tests/test.rs | 63 ++++++++++++++++++++++++++ 7 files changed, 222 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3989e6f..b600799 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,7 +2293,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.10" +version = "0.1.11" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index c5beac2..c11c93b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.10" +version = "0.1.11" edition = "2024" rust-version = "1.96" diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs index de9f85e..29f310a 100644 --- a/src/caller/message/scheduling.rs +++ b/src/caller/message/scheduling.rs @@ -15,6 +15,12 @@ pub mod request { 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, + } } pub mod response { @@ -26,6 +32,12 @@ pub mod response { pub message: String, pub data: Vec, } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct GetScheduledMessageResponse { + pub message: String, + pub data: Vec, + } } pub mod endpoint { @@ -97,6 +109,67 @@ pub mod endpoint { } } + /// 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)) diff --git a/src/caller/mod.rs b/src/caller/mod.rs index d436f86..11dff9c 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -16,6 +16,8 @@ pub mod endpoints { 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 7199ff2..ce2e588 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -130,6 +130,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/scheduling.rs b/src/repo/scheduling.rs index b03e93c..97af52c 100644 --- a/src/repo/scheduling.rs +++ b/src/repo/scheduling.rs @@ -25,3 +25,79 @@ pub async fn insert( 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), + } +} + +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, + }) +} diff --git a/tests/test.rs b/tests/test.rs index 90e8749..0660fee 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -354,6 +354,25 @@ mod request { 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), + } + } } } @@ -624,3 +643,47 @@ async fn test_schedule_message() { 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) => { + eprintln!("Response: {response:?}"); + println!("Response: {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; +} From 7873e8ed00b7da2f8c607b00205800086e715082 Mon Sep 17 00:00:00 2001 From: phoenix Date: Thu, 18 Jun 2026 18:39:24 -0400 Subject: [PATCH 05/20] Create scheduled message event (#17) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/17 --- Cargo.lock | 53 +----- Cargo.toml | 3 +- migrations/20260613211440_init.sql | 19 +++ src/caller/message/scheduling.rs | 106 +++++++++++- src/caller/mod.rs | 2 + src/config/mod.rs | 6 + src/repo/message/event.rs | 111 +++++++++++++ src/repo/{message.rs => message/mod.rs} | 3 + src/repo/message/scheduling.rs | 211 ++++++++++++++++++++++++ src/repo/mod.rs | 1 - src/repo/scheduling.rs | 103 ------------ tests/test.rs | 134 +++++++++++++-- 12 files changed, 586 insertions(+), 166 deletions(-) create mode 100644 src/repo/message/event.rs rename src/repo/{message.rs => message/mod.rs} (98%) create mode 100644 src/repo/message/scheduling.rs delete mode 100644 src/repo/scheduling.rs diff --git a/Cargo.lock b/Cargo.lock index b600799..f342aea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,22 +204,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "common-multipart-rfc7578" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08d53b5e0c302c5830cfa7511ba0edc3f241c691a95c0d184dfb761e11a6cc2" -dependencies = [ - "bytes", - "futures-core", - "futures-util", - "http", - "mime", - "mime_guess", - "rand", - "thiserror 1.0.69", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1075,7 +1059,7 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -1982,7 +1966,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -2071,7 +2055,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.18", + "thiserror", "time", "tokio", "tokio-stream", @@ -2155,7 +2139,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "time", "tracing", "uuid", @@ -2194,7 +2178,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "time", "tracing", "uuid", @@ -2220,7 +2204,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror", "time", "tracing", "url", @@ -2293,11 +2277,10 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.11" +version = "0.1.12" dependencies = [ "axum", "axum-extra", - "common-multipart-rfc7578", "futures", "josekit", "jsonwebtoken", @@ -2333,33 +2316,13 @@ dependencies = [ "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]] diff --git a/Cargo.toml b/Cargo.toml index c11c93b..253522b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.11" +version = "0.1.12" edition = "2024" rust-version = "1.96" @@ -25,6 +25,5 @@ utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.4.1-29-ad632fca16-111" } [dev-dependencies] -common-multipart-rfc7578 = { version = "0.7.0" } url = { version = "2.5.8" } tempfile = { version = "3.27.0" } diff --git a/migrations/20260613211440_init.sql b/migrations/20260613211440_init.sql index 48d8e47..7be9c7b 100644 --- a/migrations/20260613211440_init.sql +++ b/migrations/20260613211440_init.sql @@ -24,3 +24,22 @@ CREATE TABLE IF NOT EXISTS "scheduled_messages" ( 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/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs index 29f310a..5604f67 100644 --- a/src/caller/message/scheduling.rs +++ b/src/caller/message/scheduling.rs @@ -21,6 +21,21 @@ pub mod request { 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() + } + } } pub mod response { @@ -38,10 +53,18 @@ pub mod response { pub message: String, pub data: Vec, } + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct CreateScheduleMessageEventResponse { + pub message: String, + pub data: Vec, + } } pub mod endpoint { - use crate::repo::scheduling as scheduling_repo; + 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( @@ -174,4 +197,85 @@ pub mod endpoint { scheduled_time .is_some_and(|t| t - time::OffsetDateTime::now_utc() >= time::Duration::minutes(10)) } + + /// 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)) + } + } } diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 11dff9c..5d68d7f 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -18,6 +18,8 @@ pub mod endpoints { 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 creating Scheduled Message Event endpoint + pub const CREATE_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index ce2e588..6cb9a0d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -136,6 +136,12 @@ pub mod init { 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::), + ), + ) .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..c5898c6 --- /dev/null +++ b/src/repo/message/event.rs @@ -0,0 +1,111 @@ +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 { + let id: uuid::Uuid = row.try_get("id")?; + let response: String = 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_id: uuid::Uuid = row.try_get("scheduled_message_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: scheduled_message_id, + user_id, + }) +} diff --git a/src/repo/message.rs b/src/repo/message/mod.rs similarity index 98% rename from src/repo/message.rs rename to src/repo/message/mod.rs index 0e3b015..09b0414 100644 --- a/src/repo/message.rs +++ b/src/repo/message/mod.rs @@ -1,3 +1,6 @@ +pub mod event; +pub mod scheduling; + use sqlx::Row; pub async fn insert( diff --git a/src/repo/message/scheduling.rs b/src/repo/message/scheduling.rs new file mode 100644 index 0000000..246a9c9 --- /dev/null +++ b/src/repo/message/scheduling.rs @@ -0,0 +1,211 @@ +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), + } +} + +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), + } + } + + 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 d185221..2af9758 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,3 +1,2 @@ pub mod contact; pub mod message; -pub mod scheduling; diff --git a/src/repo/scheduling.rs b/src/repo/scheduling.rs deleted file mode 100644 index 97af52c..0000000 --- a/src/repo/scheduling.rs +++ /dev/null @@ -1,103 +0,0 @@ -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), - } -} - -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, - }) -} diff --git a/tests/test.rs b/tests/test.rs index 0660fee..6d2b843 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,6 +1,3 @@ -// use std::io::Write; - -// use common_multipart_rfc7578::client::multipart::{Body as MultipartBody, Form as MultipartForm}; use textsender_api::db; mod db_mgr { @@ -178,6 +175,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, @@ -191,9 +201,6 @@ 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( @@ -317,15 +324,13 @@ mod request { } pub mod scheduling { - use time::format_description::well_known::Iso8601; use tower::ServiceExt; pub async fn create_scheduled_message( app: &axum::Router, ) -> Result { - let now = time::OffsetDateTime::now_utc(); - let scheduled = now + std::time::Duration::from_secs(60 * 60); - let scheduled = match scheduled.format(&Iso8601::DEFAULT) { + 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:?}"); @@ -373,6 +378,36 @@ mod request { 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), + } + } } } @@ -628,8 +663,6 @@ async fn test_schedule_message() { match request::message::scheduling::create_scheduled_message(&app).await { Ok(response) => { - eprintln!("Response: {response:?}"); - println!("Response: {response:?}"); let resp = util::get_resp_data::< textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, >(response) @@ -653,8 +686,6 @@ async fn test_get_scheduled_message() { match request::message::scheduling::create_scheduled_message(&app).await { Ok(response) => { - eprintln!("Response: {response:?}"); - println!("Response: {response:?}"); let resp = util::get_resp_data::< textsender_api::caller::message::scheduling::response::ScheduleMessageResponse, >(response) @@ -687,3 +718,78 @@ async fn test_get_scheduled_message() { 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; +} From c1557d09f645838069f213faaf577c87309e2013 Mon Sep 17 00:00:00 2001 From: phoenix Date: Thu, 18 Jun 2026 19:59:22 -0400 Subject: [PATCH 06/20] Get scheduled message event (#18) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/18 --- src/caller/message/scheduling.rs | 77 ++++++++++++++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 6 ++ tests/test.rs | 109 +++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+) diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs index 5604f67..5f3c3b5 100644 --- a/src/caller/message/scheduling.rs +++ b/src/caller/message/scheduling.rs @@ -36,6 +36,12 @@ pub mod request { || !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, + } } pub mod response { @@ -59,6 +65,8 @@ pub mod response { pub message: String, pub data: Vec, } + + pub use CreateScheduleMessageEventResponse as GetScheduledMessageEventResponse; } pub mod endpoint { @@ -278,4 +286,73 @@ pub mod endpoint { (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)) + } } diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 5d68d7f..a65b069 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -20,6 +20,8 @@ pub mod endpoints { pub const GET_SCHEDULE_MESSAGE: &str = "/api/v1/schedule/message"; /// 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 6cb9a0d..bbf64e4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -142,6 +142,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/tests/test.rs b/tests/test.rs index 6d2b843..54cc70a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -408,6 +408,25 @@ mod request { 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), + } + } } } @@ -793,3 +812,93 @@ async fn test_create_scheduled_message_event() { 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; +} From 3a387813d79c8f40dd0a41d8bdf0e5522b20f02c Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 19 Jun 2026 16:02:53 -0400 Subject: [PATCH 07/20] Delete scheduled message event endpoint (#19) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/19 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/caller/message/scheduling.rs | 49 ++++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 8 +- src/repo/message/scheduling.rs | 19 +++++ tests/test.rs | 129 ++++++++++++++++++++++++++++++- 7 files changed, 206 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f342aea..baf05a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.12" +version = "0.1.13" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 253522b..4cc484c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.12" +version = "0.1.13" edition = "2024" rust-version = "1.96" diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs index 5f3c3b5..161673d 100644 --- a/src/caller/message/scheduling.rs +++ b/src/caller/message/scheduling.rs @@ -67,6 +67,8 @@ pub mod response { } pub use CreateScheduleMessageEventResponse as GetScheduledMessageEventResponse; + + pub use CreateScheduleMessageEventResponse as DeleteScheduledMessageEventResponse; } pub mod endpoint { @@ -355,4 +357,51 @@ pub mod endpoint { (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 a65b069..6821497 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -22,6 +22,8 @@ pub mod endpoints { 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}"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index bbf64e4..6eea69c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,7 +10,7 @@ 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; @@ -148,6 +148,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message/scheduling.rs b/src/repo/message/scheduling.rs index 246a9c9..5e3fe71 100644 --- a/src/repo/message/scheduling.rs +++ b/src/repo/message/scheduling.rs @@ -189,6 +189,25 @@ pub mod sched_msg_event { } } + 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 { diff --git a/tests/test.rs b/tests/test.rs index 54cc70a..2293c90 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -124,12 +124,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) { @@ -427,6 +425,25 @@ mod request { 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), + } + } } } @@ -902,3 +919,111 @@ async fn test_get_scheduled_message_event() { 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; +} From c98d545afe9662135c5c7a7f07a58b7dc5f80439 Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 19 Jun 2026 18:34:39 -0400 Subject: [PATCH 08/20] Update scheduled message status endpoint (#20) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/20 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/caller/message/scheduling.rs | 103 +++++++++++++++++++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 6 ++ src/repo/message/scheduling.rs | 20 ++++++ tests/test.rs | 116 +++++++++++++++++++++++++++++++ 7 files changed, 249 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index baf05a9..eae15e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.13" +version = "0.1.14" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 4cc484c..80587bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.13" +version = "0.1.14" edition = "2024" rust-version = "1.96" diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs index 161673d..af1eef7 100644 --- a/src/caller/message/scheduling.rs +++ b/src/caller/message/scheduling.rs @@ -42,6 +42,30 @@ pub mod request { 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() { + if 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 + { + true + } else { + false + } + } else { + false + } + } + } } pub mod response { @@ -69,6 +93,17 @@ pub mod response { 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 mod endpoint { @@ -208,6 +243,74 @@ pub mod endpoint { .is_some_and(|t| t - time::OffsetDateTime::now_utc() >= time::Duration::minutes(10)) } + /// 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, diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 6821497..b8ef7dd 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -18,6 +18,8 @@ pub mod endpoints { 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 diff --git a/src/config/mod.rs b/src/config/mod.rs index 6eea69c..d09b4e0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -154,6 +154,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message/scheduling.rs b/src/repo/message/scheduling.rs index 5e3fe71..36141a8 100644 --- a/src/repo/message/scheduling.rs +++ b/src/repo/message/scheduling.rs @@ -84,6 +84,26 @@ pub async fn get_with_user_id( } } +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 { diff --git a/tests/test.rs b/tests/test.rs index 2293c90..1519c09 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -377,6 +377,28 @@ mod request { } } + 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, @@ -1027,3 +1049,97 @@ async fn test_delete_scheduled_message_event() { 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; +} From e6f14917f02f68ead961aad3be5646d231857394 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 20 Jun 2026 12:03:46 -0400 Subject: [PATCH 09/20] Fetch schedule message endpoint (#21) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/21 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/caller/message/scheduling.rs | 51 +++++++++++-- src/caller/mod.rs | 2 + src/config/mod.rs | 6 ++ src/repo/message/scheduling.rs | 30 ++++++++ tests/test.rs | 126 +++++++++++++++++++++++++++++++ 7 files changed, 211 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eae15e8..2eb269f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.14" +version = "0.1.15" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 80587bf..1de4543 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.14" +version = "0.1.15" edition = "2024" rust-version = "1.96" diff --git a/src/caller/message/scheduling.rs b/src/caller/message/scheduling.rs index af1eef7..d146889 100644 --- a/src/caller/message/scheduling.rs +++ b/src/caller/message/scheduling.rs @@ -52,15 +52,10 @@ pub mod request { impl UpdateScheduledMessageStatusRequest { pub fn is_valid(&self) -> bool { if !self.status.is_empty() || !self.scheduled_message_id.is_nil() { - if self.status == textsender_models::message::scheduling::PENDING + 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 - { - true - } else { - false - } } else { false } @@ -104,6 +99,8 @@ pub mod response { pub struct ChangedStatus { pub old_status: String, } + + pub use ScheduleMessageResponse as FetchScheduledMessageResponse; } pub mod endpoint { @@ -243,6 +240,48 @@ pub mod endpoint { .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, diff --git a/src/caller/mod.rs b/src/caller/mod.rs index b8ef7dd..db43e41 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -26,6 +26,8 @@ pub mod endpoints { 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index d09b4e0..17d8485 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -160,6 +160,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message/scheduling.rs b/src/repo/message/scheduling.rs index 36141a8..1604350 100644 --- a/src/repo/message/scheduling.rs +++ b/src/repo/message/scheduling.rs @@ -84,6 +84,36 @@ pub async fn get_with_user_id( } } +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, diff --git a/tests/test.rs b/tests/test.rs index 1519c09..53e2aa0 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -377,6 +377,25 @@ mod request { } } + 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, @@ -1143,3 +1162,110 @@ async fn test_update_scheduled_message_status() { 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; +} From a9d7429d7556a93bb580c5ed27b23962e5650628 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 20 Jun 2026 13:38:23 -0400 Subject: [PATCH 10/20] Record Message Event Response endpoint (#22) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/22 --- Cargo.lock | 7 +- Cargo.toml | 4 +- src/caller/message/event.rs | 98 +++++++++++++++++ src/caller/message/mod.rs | 1 + src/caller/mod.rs | 3 + src/config/mod.rs | 13 ++- src/repo/message/event.rs | 3 +- tests/test.rs | 214 +++++++++++++++++++++++++++++++++++- 8 files changed, 335 insertions(+), 8 deletions(-) create mode 100644 src/caller/message/event.rs diff --git a/Cargo.lock b/Cargo.lock index 2eb269f..0d306d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.15" +version = "0.1.16" dependencies = [ "axum", "axum-extra", @@ -2303,14 +2303,15 @@ dependencies = [ [[package]] name = "textsender_models" -version = "0.4.1" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.1-29-ad632fca16-111#ad632fca168ec817e61dd364716f8030c9cfb0b3" +version = "0.4.3" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.3-29-384dd41690-111#384dd416902c88aa6c9c58bb5a9dfa04567ae455" dependencies = [ "const_format", "dotenvy", "josekit", "serde", "serde_json", + "sqlx", "time", "utoipa", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 1de4543..17c693c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.15" +version = "0.1.16" edition = "2024" rust-version = "1.96" @@ -22,7 +22,7 @@ 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.1-29-ad632fca16-111" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.4.3-29-384dd41690-111" } [dev-dependencies] url = { version = "2.5.8" } diff --git a/src/caller/message/event.rs b/src/caller/message/event.rs new file mode 100644 index 0000000..4341d07 --- /dev/null +++ b/src/caller/message/event.rs @@ -0,0 +1,98 @@ +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() + } + } +} + +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 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: 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)) + } + } +} diff --git a/src/caller/message/mod.rs b/src/caller/message/mod.rs index 3d28da6..5f1ba57 100644 --- a/src/caller/message/mod.rs +++ b/src/caller/message/mod.rs @@ -1,3 +1,4 @@ +pub mod event; pub mod scheduling; pub mod request { diff --git a/src/caller/mod.rs b/src/caller/mod.rs index db43e41..91f3c45 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -28,6 +28,9 @@ pub mod endpoints { 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 17d8485..7c2460c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -16,10 +16,13 @@ pub mod init { 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 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; @@ -85,7 +88,9 @@ pub mod init { scheduling_endpoints::schedule_message, ), components(schemas(contact_responses::AddContactResponse, message_responses::AddMessageResponse, - scheduling_responses::ScheduleMessageResponse)), + scheduling_responses::ScheduleMessageResponse, + event_responses::RecordMessageEventResponse + )), tags( (name = "textsender API", description = "Web API to manage texting") ) @@ -166,6 +171,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message/event.rs b/src/repo/message/event.rs index c5898c6..fd77cc3 100644 --- a/src/repo/message/event.rs +++ b/src/repo/message/event.rs @@ -89,8 +89,9 @@ pub async fn get_with_user_id( async fn parse_row( row: &sqlx::postgres::PgRow, ) -> Result { + println!("Parsing MER"); let id: uuid::Uuid = row.try_get("id")?; - let response: String = row.try_get("response")?; + 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")?; diff --git a/tests/test.rs b/tests/test.rs index 53e2aa0..1011a45 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -154,6 +154,33 @@ 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 @@ -321,6 +348,51 @@ mod request { } } + 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 mod scheduling { use tower::ServiceExt; @@ -507,7 +579,10 @@ mod request { match axum::http::Request::builder() .method(method) .uri(uri) - .header(axum::http::header::CONTENT_TYPE, "application/json") + .header( + axum::http::header::CONTENT_TYPE, + "application/json; charset=utf-8", + ) .header( axum::http::header::AUTHORIZATION, super::bearer_auth().await, @@ -1269,3 +1344,140 @@ async fn test_fetch_scheduled_message() { 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; +} From 295140a216324d5ef056678b7d0ceb691316a1d2 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 20 Jun 2026 15:05:20 -0400 Subject: [PATCH 11/20] Get Message Event Response endpoint (#23) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/23 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/caller/message/event.rs | 69 +++++++++++++++ src/caller/mod.rs | 2 + src/config/mod.rs | 6 ++ src/repo/message/event.rs | 4 +- tests/test.rs | 169 ++++++++++++++++++++++++++++++++++++ 7 files changed, 250 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d306d5..712f8f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.16" +version = "0.1.17" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 17c693c..8f7388a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.16" +version = "0.1.17" edition = "2024" rust-version = "1.96" diff --git a/src/caller/message/event.rs b/src/caller/message/event.rs index 4341d07..5635d7c 100644 --- a/src/caller/message/event.rs +++ b/src/caller/message/event.rs @@ -25,6 +25,12 @@ pub mod request { || !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 { @@ -36,6 +42,8 @@ pub mod response { pub message: String, pub data: Vec, } + + pub use RecordMessageEventResponse as GetMERResponse; } pub mod endpoint { @@ -95,4 +103,65 @@ pub mod endpoint { (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/mod.rs b/src/caller/mod.rs index 91f3c45..f37aea0 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -31,6 +31,8 @@ pub mod endpoints { /// 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"; } pub mod response { diff --git a/src/config/mod.rs b/src/config/mod.rs index 7c2460c..4d34d4c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -177,6 +177,12 @@ pub mod init { 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::), + ), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/message/event.rs b/src/repo/message/event.rs index fd77cc3..3022a99 100644 --- a/src/repo/message/event.rs +++ b/src/repo/message/event.rs @@ -96,7 +96,7 @@ async fn parse_row( 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_id: uuid::Uuid = row.try_get("scheduled_message_id")?; + 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 { @@ -106,7 +106,7 @@ async fn parse_row( message_id, status, sent: Some(sent), - scheduled_message_event_id: scheduled_message_id, + scheduled_message_event_id, user_id, }) } diff --git a/tests/test.rs b/tests/test.rs index 1011a45..df7c426 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -391,6 +391,25 @@ mod request { 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 { @@ -1481,3 +1500,153 @@ async fn test_record_message_event_response() { 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; +} From 58fa5e0d26abe7db4d3e9d960b5a0b3736fca6e2 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 20 Jun 2026 20:07:19 -0400 Subject: [PATCH 12/20] Instant message (#24) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/24 --- .env.docker.sample | 2 +- .env.local.sample | 2 +- Cargo.lock | 863 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 +- src/caller/instant.rs | 206 +++++++++ src/caller/message/event.rs | 2 +- src/caller/mod.rs | 3 + src/config/mod.rs | 6 + src/repo/message/event.rs | 2 +- 9 files changed, 1057 insertions(+), 32 deletions(-) create mode 100644 src/caller/instant.rs 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/Cargo.lock b/Cargo.lock index 712f8f3..f22145a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c93706211833310a994d19715803c622719937a03c172e66dc54902fafd4c15" + [[package]] name = "base64ct" version = "1.8.3" @@ -195,6 +223,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -204,6 +234,42 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +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 = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -251,6 +317,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" @@ -276,6 +352,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" @@ -322,7 +407,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", ] @@ -344,7 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -424,6 +509,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" @@ -486,7 +577,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -551,7 +642,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", ] @@ -589,6 +680,12 @@ dependencies = [ "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" @@ -619,6 +716,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" @@ -736,8 +839,24 @@ 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.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -748,7 +867,8 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -760,10 +880,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" @@ -894,6 +1033,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -902,6 +1042,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]] @@ -910,13 +1066,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]] @@ -1040,12 +1206,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" @@ -1088,7 +1319,7 @@ dependencies = [ "p256", "p384", "pem", - "rand", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -1189,6 +1420,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchit" version = "0.8.4" @@ -1312,7 +1549,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1571,6 +1808,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +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.45" @@ -1580,6 +1873,12 @@ 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" @@ -1593,8 +1892,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.2", + "rand_core 0.10.1", ] [[package]] @@ -1604,7 +1924,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]] @@ -1616,6 +1946,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" @@ -1663,6 +2008,52 @@ 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" @@ -1673,6 +2064,20 @@ dependencies = [ "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" @@ -1686,7 +2091,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1727,6 +2132,12 @@ dependencies = [ "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" @@ -1749,6 +2160,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +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" @@ -1806,7 +2292,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", @@ -1902,7 +2388,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1913,7 +2399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1949,7 +2435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1958,6 +2444,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" @@ -2131,7 +2633,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.6", "rsa", "serde", "sha1", @@ -2171,7 +2673,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.6", "serde", "serde_json", "sha2", @@ -2234,6 +2736,26 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swoosh" +version = "0.4.1" +source = "git+ssh://git@git.kundeng.us/phoenix/swoosh.git?tag=v0.4.1-10-9e8ec15910-871#9e8ec159102951606e30ad2013f4db8dad184fad" +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" @@ -2250,6 +2772,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" @@ -2262,6 +2787,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" @@ -2287,6 +2833,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "swoosh", "tempfile", "textsender_models", "time", @@ -2304,7 +2851,7 @@ dependencies = [ [[package]] name = "textsender_models" version = "0.4.3" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.3-29-384dd41690-111#384dd416902c88aa6c9c58bb5a9dfa04567ae455" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.3-29-7e5bc016b1-111#7e5bc016b11509d0468f10a5449c6918128f2e20" dependencies = [ "const_format", "dotenvy", @@ -2429,6 +2976,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" @@ -2481,12 +3038,15 @@ checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", + "futures-util", "http", "http-body", "pin-project-lite", "tokio", + "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2559,6 +3119,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" @@ -2604,6 +3170,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" @@ -2705,6 +3277,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" @@ -2748,6 +3329,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.125" @@ -2802,6 +3393,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -2814,6 +3418,35 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "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]] name = "whoami" version = "1.6.1" @@ -2839,13 +3472,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]] @@ -2863,13 +3543,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]] @@ -2878,42 +3591,138 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 8f7388a..2a4425c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ 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.3-29-384dd41690-111" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.4.3-29-7e5bc016b1-111" } +swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.4.1-10-9e8ec15910-871" } [dev-dependencies] url = { version = "2.5.8" } diff --git a/src/caller/instant.rs b/src/caller/instant.rs new file mode 100644 index 0000000..b0344bb --- /dev/null +++ b/src/caller/instant.rs @@ -0,0 +1,206 @@ +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 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 mut results: Vec = Vec::new(); + let pp = swoosh::twilio::types::Parameters { + schedule: false, + schedule_at: None, + }; + + for contact in &contacts { + match swoosh::twilio::api::send_mssage( + &message, contact, &pp, &t_config, + ) + .await + { + Ok(result) => { + println!("Response: {result:?}"); + let val = swoosh::twilio::api::response_to_json(result).await; + // results.push(val); + 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)) + } + } + + async fn load_config() + -> Result { + let mut config = textsender_models::config::auxiliary::TwilioConfig { + ..Default::default() + }; + + let auth_sid_var = textsender_models::envy::environment::get_env("TWILIO_AUTH_SID").await; + println!("{auth_sid_var:?}"); + let service_sid_var = + textsender_models::envy::environment::get_env("TWILIO_SERVICE_SID").await; + println!("{service_sid_var:?}"); + let auth_token_var = + textsender_models::envy::environment::get_env("TWILIO_AUTH_TOKEN").await; + println!("{auth_token_var:?}"); + let phone_number_var = + textsender_models::envy::environment::get_env("TWILIO_PHONE_NUMBER").await; + println!("{phone_number_var:?}"); + + if auth_sid_var.value.is_empty() + || service_sid_var.value.is_empty() + || auth_token_var.value.is_empty() + || phone_number_var.value.is_empty() + { + Err(std::io::Error::other("Config not set")) + } else { + config.account_sid = auth_sid_var.value; + config.service_sid = service_sid_var.value; + config.auth_token = auth_token_var.value; + config.phone_number = phone_number_var.value; + Ok(config) + } + } +} diff --git a/src/caller/message/event.rs b/src/caller/message/event.rs index 5635d7c..a3b79c4 100644 --- a/src/caller/message/event.rs +++ b/src/caller/message/event.rs @@ -75,7 +75,7 @@ pub mod endpoint { if payload.is_valid() { let mut mer = textsender_models::message::event::MessageEventResponse { - scheduled_message_event_id: payload.scheduled_message_event_id, + scheduled_message_event_id: Some(payload.scheduled_message_event_id), user_id: payload.user_id, sent: payload.sent, status: payload.status, diff --git a/src/caller/mod.rs b/src/caller/mod.rs index f37aea0..8b747d6 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -1,4 +1,5 @@ pub mod contact; +pub mod instant; pub mod message; pub mod endpoints { @@ -33,6 +34,8 @@ pub mod endpoints { "/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 4d34d4c..a52ebb8 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -183,6 +183,12 @@ pub mod init { 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 index 3022a99..31eb864 100644 --- a/src/repo/message/event.rs +++ b/src/repo/message/event.rs @@ -106,7 +106,7 @@ async fn parse_row( message_id, status, sent: Some(sent), - scheduled_message_event_id, + scheduled_message_event_id: Some(scheduled_message_event_id), user_id, }) } From 276d5b60d45846b1d543d1e851f9003ffab3aa97 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 20 Jun 2026 20:15:04 -0400 Subject: [PATCH 13/20] Postgres update (#25) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/25 --- .gitea/workflows/tests.yml | 2 +- .gitea/workflows/workflow.yml | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- docker-compose.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml index 5d64798..71d639f 100644 --- a/.gitea/workflows/tests.yml +++ b/.gitea/workflows/tests.yml @@ -17,7 +17,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/.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 f22145a..6ba0189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2823,7 +2823,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.17" +version = "0.1.19" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 2a4425c..f9b8d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.17" +version = "0.1.19" edition = "2024" rust-version = "1.96" diff --git a/docker-compose.yml b/docker-compose.yml index 7fe4794..a7dc00e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,7 +66,7 @@ services: # 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 From d37d087d447021c710b4b7a7abc036fb106595e0 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 20 Jun 2026 23:11:14 -0400 Subject: [PATCH 14/20] bump: sqlx (#26) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/26 --- Cargo.lock | 446 +++++++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 4 +- tests/test.rs | 17 +- 3 files changed, 406 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ba0189..cefbaf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,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" @@ -260,6 +269,12 @@ 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" @@ -422,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" @@ -431,7 +464,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -486,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" @@ -522,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", @@ -548,7 +592,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -570,11 +614,11 @@ 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 0.6.4", @@ -619,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" @@ -680,6 +734,17 @@ 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" @@ -692,6 +757,12 @@ 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" @@ -911,7 +982,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]] @@ -929,6 +1011,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" @@ -957,7 +1048,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]] @@ -966,7 +1066,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]] @@ -1023,6 +1132,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -1314,7 +1432,7 @@ dependencies = [ "base64", "ed25519-dalek", "getrandom 0.2.17", - "hmac", + "hmac 0.12.1", "js-sys", "p256", "p384", @@ -1323,7 +1441,7 @@ dependencies = [ "rsa", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "signature", "simple_asn1", "zeroize", @@ -1439,7 +1557,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]] @@ -1648,7 +1776,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1660,7 +1788,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -2060,7 +2188,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -2085,7 +2213,7 @@ 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", @@ -2128,7 +2256,7 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -2389,7 +2517,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "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]] @@ -2400,7 +2539,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "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]] @@ -2434,7 +2584,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -2522,11 +2672,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]] @@ -2546,7 +2709,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink", + "hashlink 0.10.0", "indexmap", "log", "memchr", @@ -2555,7 +2718,44 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", + "smallvec", + "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", @@ -2574,8 +2774,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", ] @@ -2594,16 +2807,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" @@ -2616,7 +2855,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -2625,27 +2864,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 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", "time", "tracing", "uuid", - "whoami", + "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", ] [[package]] @@ -2660,31 +2927,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 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", - "sqlx-core", + "sqlx-core 0.8.6", "stringprep", "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]] @@ -2694,7 +2998,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", @@ -2705,7 +3009,33 @@ dependencies = [ "percent-encoding", "serde", "serde_urlencoded", - "sqlx-core", + "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", @@ -2823,7 +3153,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.19" +version = "0.1.20" dependencies = [ "axum", "axum-extra", @@ -2832,7 +3162,7 @@ dependencies = [ "jsonwebtoken", "serde", "serde_json", - "sqlx", + "sqlx 0.9.0", "swoosh", "tempfile", "textsender_models", @@ -2858,7 +3188,7 @@ dependencies = [ "josekit", "serde", "serde_json", - "sqlx", + "sqlx 0.8.6", "time", "utoipa", "uuid", @@ -3457,6 +3787,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" diff --git a/Cargo.toml b/Cargo.toml index f9b8d64..65bc273 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.19" +version = "0.1.20" edition = "2024" rust-version = "1.96" @@ -16,7 +16,7 @@ tower-http = { version = "0.6.11", features = ["cors", "timeout"] } tracing-subscriber = "0.3.23" futures = { version = "0.3.32" } 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"] } jsonwebtoken = { version = "10.4.0", features = ["rust_crypto"] } josekit = { version = "0.10.3" } diff --git a/tests/test.rs b/tests/test.rs index df7c426..09c8172 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -31,8 +31,13 @@ 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), } @@ -66,8 +71,12 @@ mod db_mgr { 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(()) } From 708e3e86f3e1598b40b3c4f1f70f7309c7abc554 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sun, 21 Jun 2026 21:13:06 -0400 Subject: [PATCH 15/20] Twilio Config refactor (#27) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/27 --- Cargo.lock | 6 ++--- Cargo.toml | 6 ++--- src/caller/instant.rs | 61 +++++++++---------------------------------- 3 files changed, 19 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cefbaf4..b4acb29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3069,7 +3069,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swoosh" version = "0.4.1" -source = "git+ssh://git@git.kundeng.us/phoenix/swoosh.git?tag=v0.4.1-10-9e8ec15910-871#9e8ec159102951606e30ad2013f4db8dad184fad" +source = "git+ssh://git@git.kundeng.us/phoenix/swoosh.git?tag=v0.4.1-10-6bdec12084-871#6bdec1208499dfa9962efd89b549d1bfa5441d85" dependencies = [ "base64-ng", "const_format", @@ -3153,7 +3153,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.20" +version = "0.1.21" dependencies = [ "axum", "axum-extra", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "textsender_models" version = "0.4.3" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.3-29-7e5bc016b1-111#7e5bc016b11509d0468f10a5449c6918128f2e20" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.3-29-720f3d8f75-111#720f3d8f7515d8cd20a9cd73086bd6c1ab857651" dependencies = [ "const_format", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 65bc273..d347808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.20" +version = "0.1.21" edition = "2024" rust-version = "1.96" @@ -22,8 +22,8 @@ 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.3-29-7e5bc016b1-111" } -swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.4.1-10-9e8ec15910-871" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.4.3-29-720f3d8f75-111" } +swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.4.1-10-6bdec12084-871" } [dev-dependencies] url = { version = "2.5.8" } diff --git a/src/caller/instant.rs b/src/caller/instant.rs index b0344bb..4b1fcb8 100644 --- a/src/caller/instant.rs +++ b/src/caller/instant.rs @@ -81,36 +81,34 @@ pub mod endpoint { match message_repo::get(&pool, &payload.message_id).await { Ok(message) => { println!("Valid message"); - let t_config = match 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 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 mut results: Vec = Vec::new(); let pp = swoosh::twilio::types::Parameters { schedule: false, schedule_at: None, }; for contact in &contacts { - match swoosh::twilio::api::send_mssage( + match swoosh::twilio::api::send_message( &message, contact, &pp, &t_config, ) .await { Ok(result) => { - println!("Response: {result:?}"); let val = swoosh::twilio::api::response_to_json(result).await; - // results.push(val); let mer = textsender_models::message::event::MessageEventResponse { user_id: payload.user_id, // TODO: Make this more accurate @@ -170,37 +168,4 @@ pub mod endpoint { (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) } } - - async fn load_config() - -> Result { - let mut config = textsender_models::config::auxiliary::TwilioConfig { - ..Default::default() - }; - - let auth_sid_var = textsender_models::envy::environment::get_env("TWILIO_AUTH_SID").await; - println!("{auth_sid_var:?}"); - let service_sid_var = - textsender_models::envy::environment::get_env("TWILIO_SERVICE_SID").await; - println!("{service_sid_var:?}"); - let auth_token_var = - textsender_models::envy::environment::get_env("TWILIO_AUTH_TOKEN").await; - println!("{auth_token_var:?}"); - let phone_number_var = - textsender_models::envy::environment::get_env("TWILIO_PHONE_NUMBER").await; - println!("{phone_number_var:?}"); - - if auth_sid_var.value.is_empty() - || service_sid_var.value.is_empty() - || auth_token_var.value.is_empty() - || phone_number_var.value.is_empty() - { - Err(std::io::Error::other("Config not set")) - } else { - config.account_sid = auth_sid_var.value; - config.service_sid = service_sid_var.value; - config.auth_token = auth_token_var.value; - config.phone_number = phone_number_var.value; - Ok(config) - } - } } From c982d5595a93254cdbc7031a597680e946652de3 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 27 Jun 2026 17:41:21 -0400 Subject: [PATCH 16/20] Adding catapult to docker --- docker-compose.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a7dc00e..6dcd817 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,22 +45,22 @@ 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 From 5220646eb9809aeae324458d2ecfcf3853a45fc1 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 27 Jun 2026 17:49:34 -0400 Subject: [PATCH 17/20] bump: swoosh and textsender_models --- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4acb29..c53ea47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3068,8 +3068,8 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swoosh" -version = "0.4.1" -source = "git+ssh://git@git.kundeng.us/phoenix/swoosh.git?tag=v0.4.1-10-6bdec12084-871#6bdec1208499dfa9962efd89b549d1bfa5441d85" +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", @@ -3180,8 +3180,8 @@ dependencies = [ [[package]] name = "textsender_models" -version = "0.4.3" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.3-29-720f3d8f75-111#720f3d8f7515d8cd20a9cd73086bd6c1ab857651" +version = "0.4.10" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.4.10#c1d391939baf94df09ee405a1f0bf269690f7dec" dependencies = [ "const_format", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index d347808..117e389 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ 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.3-29-720f3d8f75-111" } -swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.4.1-10-6bdec12084-871" } +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] url = { version = "2.5.8" } From 09e2a0e78eafa9da9949d74cb9992fddacc01824 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 27 Jun 2026 17:50:47 -0400 Subject: [PATCH 18/20] cargo update --- Cargo.lock | 259 +++++++++++------------------------------------------ 1 file changed, 52 insertions(+), 207 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c53ea47..8efb713 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" @@ -170,9 +170,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64-ng" -version = "1.1.0" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c93706211833310a994d19715803c622719937a03c172e66dc54902fafd4c15" +checksum = "735259e24200bce69feb75fcb2f79f36129e54c3f3139da15880520988cd840c" [[package]] name = "base64ct" @@ -221,15 +221,15 @@ 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", @@ -251,9 +251,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -932,16 +932,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] @@ -1134,9 +1132,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] @@ -1285,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" @@ -1414,9 +1406,9 @@ dependencies = [ [[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", @@ -1471,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" @@ -1534,9 +1520,9 @@ 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" @@ -1908,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" @@ -1938,9 +1914,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1958,9 +1934,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", @@ -1994,9 +1970,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2041,7 +2017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -2290,9 +2266,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", @@ -3088,9 +3064,9 @@ dependencies = [ [[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", @@ -3145,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", @@ -3225,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", @@ -3245,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", @@ -3569,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", @@ -3628,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]] @@ -3648,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", @@ -3661,9 +3628,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.75" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3671,9 +3638,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3681,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", @@ -3694,35 +3661,13 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -3736,23 +3681,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4059,100 +3992,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" 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" @@ -4292,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" From 44b1e50dc493e4587d0b8aba4517b881b8e75086 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 27 Jun 2026 17:51:22 -0400 Subject: [PATCH 19/20] bump: textsender_api --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8efb713..0fe8c99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3129,7 +3129,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.21" +version = "0.2.0" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 117e389..4d2860e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.21" +version = "0.2.0" edition = "2024" rust-version = "1.96" From 56cbd7710f30dae7e881e3da3b13274038048815 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 27 Jun 2026 17:52:47 -0400 Subject: [PATCH 20/20] Updated workflow --- .gitea/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml index 71d639f..7f58e3f 100644 --- a/.gitea/workflows/tests.yml +++ b/.gitea/workflows/tests.yml @@ -3,7 +3,7 @@ name: Rust Build on: pull_request: branches: - - alot_of_changes + - next concurrency: