Compare commits

..
Author SHA1 Message Date
phoenix fb07761c25 bump: swoosh
Release Tagging / release (pull_request) Successful in 30s
swoosh Build / Check (pull_request) Successful in 30s
swoosh Build / Test Suite (pull_request) Successful in 34s
swoosh Build / Rustfmt (pull_request) Successful in 29s
swoosh Build / Clippy (pull_request) Successful in 31s
swoosh Build / build (pull_request) Successful in 28s
2026-06-04 21:07:27 -04:00
phoenix b85b329ea5 Cleanup 2026-06-04 21:07:03 -04:00
phoenix c9d167da8d Changed name of function
swoosh Build / Check (pull_request) Successful in 31s
swoosh Build / Test Suite (pull_request) Successful in 28s
Release Tagging / release (pull_request) Successful in 1m10s
swoosh Build / Rustfmt (pull_request) Failing after 31s
swoosh Build / Clippy (pull_request) Successful in 33s
swoosh Build / build (pull_request) Successful in 33s
2026-06-04 21:06:08 -04:00
phoenix eeb46dc84d Added function to convert response to a json value 2026-06-04 21:05:11 -04:00
9 changed files with 600 additions and 1166 deletions
-73
View File
@@ -1,73 +0,0 @@
name: swoosh pr
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- 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 check
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt
- 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
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add clippy
- 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 clippy -- -D warnings
+2 -2
View File
@@ -13,14 +13,14 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0 # Important for git describe --tags
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.97
toolchain: 1.95
components: cargo
- name: Extract Version from Cargo.toml
+14 -19
View File
@@ -4,21 +4,19 @@ on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
pull_request:
branches:
- main
jobs:
check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
toolchain: 1.95
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
@@ -34,11 +32,10 @@ jobs:
name: Test Suite
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
toolchain: 1.95
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
@@ -54,11 +51,10 @@ jobs:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
toolchain: 1.95
- run: rustup component add rustfmt
- run: |
mkdir -p ~/.ssh
@@ -75,11 +71,10 @@ jobs:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
toolchain: 1.95
- run: rustup component add clippy
- run: |
mkdir -p ~/.ssh
@@ -96,11 +91,10 @@ jobs:
name: build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
toolchain: 1.95
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
@@ -111,3 +105,4 @@ jobs:
ssh-add -v ~/.ssh/textsender-models_deploy_key
cargo build
Generated
+509 -757
View File
File diff suppressed because it is too large Load Diff
+15 -8
View File
@@ -1,17 +1,24 @@
[package]
name = "swoosh"
version = "0.5.4"
version = "0.3.1"
edition = "2024"
rust-version = "1.97"
license = "MIT"
rust-version = "1.95"
description = "Library to send text messages"
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150" }
reqwest = { version = "0.13.4", features = ["form", "json", "blocking", "multipart", "stream"] }
time = { version = "0.3.53", features = ["formatting", "macros", "parsing", "serde"] }
base64-ng = { version = "1.3.7" }
schedtxt_models = { git = "ssh://git@git.kundeng.us/phoenix/schedtxt_models.git", tag = "v0.5.3", features = ["config", "contact", "message"] }
serde_json = { version = "1.0.149" }
tokio = { version = "1.52.2", features = ["full"] }
futures = { version = "0.3.32" }
http = { version = "1.4.0" }
reqwest = { version = "0.13.3", features = ["form", "json", "blocking", "multipart", "stream"] }
rand = { version = "0.10.1" }
time = { version = "0.3.47", features = ["formatting", "macros", "parsing", "serde"] }
uuid = { version = "1.23.1", features = ["v4", "serde"] }
base64-ng = { version = "1.0.6" }
const_format = { version = "0.2.36" }
josekit = { version = "0.10.3" }
textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender-models.git", tag = "v0.2.5-v0.3.0-migrate-38dbcb09f7-111" }
[dev-dependencies]
tempfile = { version = "3.27.0" }
-22
View File
@@ -1,22 +0,0 @@
Copyright (c) 2026 Kun Deng.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
-3
View File
@@ -1,3 +0,0 @@
# swoosh
A library used to send text messages and is used in conjunction of the `schedtxt`
project.
-106
View File
@@ -1,107 +1 @@
pub mod twilio;
#[derive(Default, Debug)]
pub struct SendMsg {
config: VendorConfig,
message: Message,
recipient: Recipient,
}
impl SendMsg {
pub fn load_config(
&mut self,
auth_token: &str,
phone_number: &str,
service_sid: &str,
account_sid: &str,
) {
self.config = VendorConfig {
auth_token: auth_token.to_string(),
phone_number: phone_number.to_string(),
service_sid: service_sid.to_string(),
account_sid: account_sid.to_string(),
}
}
pub fn load_message(&mut self, content: &str) {
self.message = Message {
content: content.to_string(),
}
}
pub fn load_recipient(&mut self, recpient: &str) {
self.recipient = Recipient {
phone_number: recpient.to_string(),
}
}
pub async fn send_message(
&self,
param: &crate::twilio::types::Parameters,
) -> Result<reqwest::Response, std::io::Error> {
if self.recipient.phone_number.is_empty() {
Err(std::io::Error::other("Recipient not provided"))
} else if self.message.content.is_empty() {
Err(std::io::Error::other("Message not provided"))
} else if self.config.phone_number.is_empty()
|| self.config.service_sid.is_empty()
|| self.config.auth_token.is_empty()
|| self.config.account_sid.is_empty()
{
Err(std::io::Error::other("Config not populated"))
} else {
let client = match reqwest::Client::builder().build() {
Ok(client) => client,
Err(err) => {
return Err(std::io::Error::other(err));
}
};
let url = twilio::api::twilio_url(&self.config.account_sid);
let headers = match twilio::api::init_headers(
&self.config.account_sid,
&self.config.auth_token,
) {
Ok(headers) => headers,
Err(err) => {
return Err(std::io::Error::other(err));
}
};
let params = twilio::api::initial_params(
&self.recipient.phone_number,
&self.message.content,
&self.config.phone_number,
&self.config.service_sid,
param,
);
let request = client.post(url).headers(headers).form(&params);
match request.send().await {
Ok(response) => match response.status() {
reqwest::StatusCode::CREATED => Ok(response),
status => Err(std::io::Error::other(format!(
"Unaccounted status: {status:?}"
))),
},
Err(err) => Err(std::io::Error::other(err)),
}
}
}
}
#[derive(Default, Debug)]
pub struct VendorConfig {
pub auth_token: String,
pub phone_number: String,
pub service_sid: String,
pub account_sid: String,
}
#[derive(Default, Debug)]
pub struct Message {
pub content: String,
}
#[derive(Default, Debug)]
pub struct Recipient {
pub phone_number: String,
}
+60 -176
View File
@@ -1,7 +1,5 @@
pub mod types;
/// Converts the response into a json value
/// That way you do not need to share swoosh's dependency
pub async fn response_to_json(response: reqwest::Response) -> serde_json::Value {
match response.json().await {
Ok(json_body) => json_body,
@@ -9,21 +7,18 @@ pub async fn response_to_json(response: reqwest::Response) -> serde_json::Value
}
}
/// Backwards compatible
pub use send_message as send_mssage;
/// Send a message
pub async fn send_message(
msg: &schedtxt_models::message::Message,
contact: &schedtxt_models::contact::Contact,
param: &crate::twilio::types::Parameters,
config: &schedtxt_models::config::auxiliary::TwilioConfig,
pub async fn send_mssage(
msg: &textsender_models::message::Message,
contact: &textsender_models::contact::Contact,
param: crate::twilio::types::Parameters,
config: &textsender_models::config::auxiliary::TwilioConfig,
) -> Result<reqwest::Response, std::io::Error> {
if config.account_sid.is_empty() {
Err(std::io::Error::other("Account SID is empty"))
} else if config.auth_token.is_empty() {
Err(std::io::Error::other(" Auth token is empty"))
} else {
let now = time::OffsetDateTime::now_utc();
match reqwest::Client::builder().build() {
Ok(client) => {
let mut headers = reqwest::header::HeaderMap::new();
@@ -35,7 +30,33 @@ pub async fn send_message(
let auth = generate_auth(config).unwrap();
headers.insert("Authorization", auth.parse().unwrap());
let params = init_params(contact, msg, config, param);
let mut params = std::collections::HashMap::new();
params.insert("To", contact.phone_number.as_str());
// params.insert("StatusCallback", "http://OjQozHznkhNTTR.vpnrM1zdXFuiQ");
// params.insert("MaxPrice", "1");
params.insert("ProvideFeedback", "true");
// params.insert("Attempt", "5");
// params.insert("ValidityPeriod", "1537");
params.insert("ForceDelivery", "false");
params.insert("ContentRetention", "retain");
params.insert("AddressRetention", "obfuscate");
params.insert("SmartEncoded", "true");
// params.insert("PersistentAction", "string");
// params.insert("PersistentAction", "string");
params.insert("ShortenUrls", "true");
let date = match param.schedule_at {
Some(date_value) => date_value.to_string(),
None => String::new(),
};
if param.schedule && is_scheduleable(&Some(now), &param.schedule_at) {
params.insert("ScheduleType", "fixed");
params.insert("SendAt", date.as_str());
}
params.insert("SendAsMms", "true");
params.insert("RiskCheck", "enable");
params.insert("From", config.phone_number.as_str());
params.insert("MessagingServiceSid", config.service_sid.as_str());
params.insert("Body", msg.content.as_str());
let url = format!(
"https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json",
@@ -45,7 +66,7 @@ pub async fn send_message(
let request = client.post(url).headers(headers).form(&params);
match request.send().await {
Ok(response) => match response.status() {
reqwest::StatusCode::CREATED => Ok(response),
reqwest::StatusCode::OK => Ok(response),
status => Err(std::io::Error::other(format!(
"Unaccounted status: {status:?}"
))),
@@ -58,162 +79,8 @@ pub async fn send_message(
}
}
pub fn twilio_url(account_sid: &str) -> String {
format!(
"https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json",
account_sid
)
}
pub fn init_headers(
account_sid: &str,
auth_token: &str,
) -> Result<reqwest::header::HeaderMap, std::io::Error> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
"Content-Type",
match "application/x-www-form-urlencoded".parse() {
Ok(value) => value,
Err(err) => {
return Err(std::io::Error::other(err));
}
},
);
let auth = match g_auth(account_sid, auth_token) {
Ok(auth) => auth,
Err(err) => {
return Err(std::io::Error::other(err));
}
};
headers.insert(
"Authorization",
match format!("Basic {auth}").parse() {
Ok(auth) => auth,
Err(err) => {
return Err(std::io::Error::other(err));
}
},
);
Ok(headers)
}
pub fn initial_params(
recpient: &str,
content: &str,
config_source_phone_number: &str,
config_service_sid: &str,
param: &crate::twilio::types::Parameters,
) -> std::collections::HashMap<String, String> {
let now = time::OffsetDateTime::now_utc();
let mut params = std::collections::HashMap::new();
params.insert(String::from("To"), recpient.to_string());
params.insert(String::from("ProvideFeedback"), String::from("true"));
params.insert(String::from("ForceDelivery"), String::from("false"));
params.insert(String::from("ContentRetention"), String::from("retain"));
params.insert(String::from("AddressRetention"), String::from("obfuscate"));
params.insert(String::from("SmartEncoded"), String::from("true"));
params.insert(String::from("ShortenUrls"), String::from("true"));
let date = match param.schedule_at {
Some(date_value) => match convert_time_to_iso(date_value) {
Ok(converted) => converted,
Err(err) => {
eprintln!("Error: {err:?}");
String::new()
}
},
None => String::new(),
};
let scheduled_at = match param.schedule_at {
Some(s) => s,
None => time::OffsetDateTime::now_utc(),
};
if param.schedule && is_scheduleable(Some(&now), Some(&scheduled_at)) {
params.insert(String::from("ScheduleType"), String::from("fixed"));
params.insert(String::from("SendAt"), date.clone());
}
params.insert(String::from("SendAsMms"), String::from("true"));
params.insert(String::from("RiskCheck"), String::from("enable"));
params.insert(String::from("From"), config_source_phone_number.to_string());
params.insert(
String::from("MessagingServiceSid"),
config_service_sid.to_string(),
);
params.insert(String::from("Body"), content.to_string());
params
}
fn init_params(
contact: &schedtxt_models::contact::Contact,
message: &schedtxt_models::message::Message,
config: &schedtxt_models::config::auxiliary::TwilioConfig,
param: &crate::twilio::types::Parameters,
) -> std::collections::HashMap<String, String> {
let now = time::OffsetDateTime::now_utc();
let mut params = std::collections::HashMap::new();
params.insert(String::from("To"), contact.phone_number.clone());
params.insert(String::from("ProvideFeedback"), String::from("true"));
params.insert(String::from("ForceDelivery"), String::from("false"));
params.insert(String::from("ContentRetention"), String::from("retain"));
params.insert(String::from("AddressRetention"), String::from("obfuscate"));
params.insert(String::from("SmartEncoded"), String::from("true"));
params.insert(String::from("ShortenUrls"), String::from("true"));
let date = match param.schedule_at {
Some(date_value) => match convert_time_to_iso(date_value) {
Ok(converted) => converted,
Err(err) => {
eprintln!("Error: {err:?}");
String::new()
}
},
None => String::new(),
};
let scheduled_at = match param.schedule_at {
Some(s) => s,
None => time::OffsetDateTime::now_utc(),
};
if param.schedule && is_scheduleable(Some(&now), Some(&scheduled_at)) {
params.insert(String::from("ScheduleType"), String::from("fixed"));
params.insert(String::from("SendAt"), date.clone());
}
params.insert(String::from("SendAsMms"), String::from("true"));
params.insert(String::from("RiskCheck"), String::from("enable"));
params.insert(String::from("From"), config.phone_number.clone());
params.insert(
String::from("MessagingServiceSid"),
config.service_sid.clone(),
);
params.insert(String::from("Body"), message.content.clone());
/*
let mut params = std::collections::HashMap::new();
params.insert("StatusCallback", "http://OjQozHznkhNTTR.vpnrM1zdXFuiQ");
params.insert("MaxPrice", "1");
params.insert("ProvideFeedback", "true");
params.insert("Attempt", "5");
params.insert("ValidityPeriod", "1537");
params.insert("ForceDelivery", "false");
params.insert("ContentRetention", "retain");
params.insert("AddressRetention", "obfuscate");
params.insert("SmartEncoded", "true");
params.insert("PersistentAction", "string");
params.insert("PersistentAction", "string");
params.insert("ShortenUrls", "true");
params.insert("SendAsMms", "true");
params.insert("RiskCheck", "enable");
*/
params
}
fn convert_time_to_iso(time: time::OffsetDateTime) -> Result<String, time::error::Format> {
use time::format_description::well_known::Iso8601;
time.format(&Iso8601::DEFAULT)
}
fn generate_auth(
config: &schedtxt_models::config::auxiliary::TwilioConfig,
config: &textsender_models::config::auxiliary::TwilioConfig,
) -> Result<String, base64_ng::EncodeError> {
let input = format!("{}:{}", config.account_sid, config.auth_token);
match base64_ng::encode(input.as_bytes()) {
@@ -221,23 +88,40 @@ fn generate_auth(
Err(err) => Err(err),
}
}
/*
let mut params = std::collections::HashMap::new();
params.insert("StatusCallback", "http://OjQozHznkhNTTR.vpnrM1zdXFuiQ");
params.insert("MaxPrice", "1");
params.insert("ProvideFeedback", "true");
params.insert("Attempt", "5");
params.insert("ValidityPeriod", "1537");
params.insert("ForceDelivery", "false");
params.insert("ContentRetention", "retain");
params.insert("AddressRetention", "obfuscate");
params.insert("SmartEncoded", "true");
params.insert("PersistentAction", "string");
params.insert("PersistentAction", "string");
params.insert("ShortenUrls", "true");
params.insert("SendAsMms", "true");
params.insert("RiskCheck", "enable");
fn g_auth(account_sid: &str, auth_token: &str) -> Result<String, base64_ng::EncodeError> {
let input = format!("{}:{}", account_sid, auth_token);
base64_ng::encode(input.as_bytes())
let response = request.send().await?;
let body = response.text().await?;
println!("{}", body);
Ok(())
}
const DEFAULT_SCHEDULING_SECONDS: i64 = 300;
*/
fn is_scheduleable(
now: Option<&time::OffsetDateTime>,
scheduled: Option<&time::OffsetDateTime>,
now: &Option<time::OffsetDateTime>,
scheduled: &Option<time::OffsetDateTime>,
) -> bool {
match scheduled {
Some(schedule_at) => {
let early = now
.unwrap()
.checked_add(time::Duration::seconds(DEFAULT_SCHEDULING_SECONDS))
.checked_add(time::Duration::seconds(300))
.unwrap();
*schedule_at > early
}