Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
495 changes: 465 additions & 30 deletions unmnemonic_devices_vrs/Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion unmnemonic_devices_vrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
handlebars = { version = "4", features = ["dir_source"] }
http = "0.2.9"
mime = "0.3.16"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
toml = "0.7.3"
tower-http = { version = "0.4", features = ["trace"] }
Expand All @@ -33,11 +34,13 @@ sqlx = { version = "0.7.0-alpha.2", features = [
"uuid",
] }
urlencoding = "2.1.2"
base64 = "0.21.4"
openapi = { version = "1.50.1", path = "twilio-rust" }

[dependencies.uuid]
version = "1.3.1"
features = ["serde", "v4"]

[dev-dependencies]
speculoos = "0.11"
reqwest = "0.11"
wiremock = "0.5.19"
49 changes: 49 additions & 0 deletions unmnemonic_devices_vrs/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::collections::HashMap;

#[derive(Debug, Default)]
pub struct Config {
pub database_url: String,
pub twilio_account_sid: String,
pub twilio_auth_token: String,
pub twilio_url: String,
}

pub trait ConfigProvider {
fn get_config(&self) -> &Config;
}

pub struct EnvVarProvider(Config);

impl EnvVarProvider {
pub fn new(args: HashMap<String, String>) -> Self {
let config = Config {
database_url: args
.get("DATABASE_URL")
.expect("Missing database URL")
.to_string(),
twilio_account_sid: args
.get("TWILIO_ACCOUNT_SID")
.expect("Missing Twilio account SID")
.to_string(),
twilio_auth_token: args
.get("TWILIO_AUTH_TOKEN")
.expect("Missing Twilio auth token")
.to_string(),
twilio_url: "https://api.twilio.com".to_string(),
};

EnvVarProvider(config)
}
}

impl ConfigProvider for EnvVarProvider {
fn get_config(&self) -> &Config {
&self.0
}
}

impl Default for EnvVarProvider {
fn default() -> Self {
Self::new(HashMap::new())
}
}
15 changes: 13 additions & 2 deletions unmnemonic_devices_vrs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod config;
pub mod helpers;
pub mod render_xml;
pub mod routes;
Expand All @@ -17,9 +18,15 @@ use crate::routes::*;

pub type AppEngine = Engine<Handlebars<'static>>;

pub struct InjectableServices {
pub db: PgPool,
pub twilio_address: String,
}

#[derive(Clone)]
pub struct AppState {
db: PgPool,
twilio_address: String,
engine: AppEngine,
prompts: Prompts,
}
Expand All @@ -30,7 +37,7 @@ pub struct Prompts {
tables: HashMap<String, HashMap<String, String>>,
}

pub async fn app(db: PgPool) -> Router {
pub async fn app(services: InjectableServices) -> Router {
let mut hbs = Handlebars::new();
hbs.register_templates_directory(".hbs", "src/templates")
.expect("Failed to register templates directory");
Expand All @@ -42,7 +49,8 @@ pub async fn app(db: PgPool) -> Router {
toml::from_str(&prompts_string).expect("Failed to parse the prompts file");

let shared_state = AppState {
db,
db: services.db,
twilio_address: services.twilio_address,
engine: Engine::from(hbs),
prompts,
};
Expand Down Expand Up @@ -83,6 +91,9 @@ pub async fn app(db: PgPool) -> Router {
.route("/teams/:id", post(post_team))
.route("/teams/:id/confirm", get(get_confirm_team))
.route("/teams/:id/confirm", post(post_confirm_team))
//
// admin routes
.route("/calls", get(get_calls))
.with_state(shared_state)
.layer(tower_http::trace::TraceLayer::new_for_http())
}
19 changes: 15 additions & 4 deletions unmnemonic_devices_vrs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ use axum::Server;
use sqlx::PgPool;
use std::env;
use std::net::{SocketAddr, TcpListener};
use unmnemonic_devices_vrs::app;
use unmnemonic_devices_vrs::config::{ConfigProvider, EnvVarProvider};
use unmnemonic_devices_vrs::{app, InjectableServices};

#[tokio::main]
async fn main() {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
let db = PgPool::connect(&database_url).await.unwrap();
let env_config_provider = EnvVarProvider::new(env::vars().collect());
let config = &env_config_provider.get_config();

let database_url = &config.database_url;
let db = PgPool::connect(database_url).await.unwrap();

let listener_address = "127.0.0.1:3000";
let listener = TcpListener::bind(listener_address.parse::<SocketAddr>().unwrap()).unwrap();
Expand All @@ -20,7 +24,14 @@ async fn main() {

Server::from_tcp(listener)
.expect("Failed to listen")
.serve(app(db).await.into_make_service())
.serve(
app(InjectableServices {
db,
twilio_address: config.twilio_url.to_string(),
})
.await
.into_make_service(),
)
.await
.expect("Failed to start server")
}
83 changes: 83 additions & 0 deletions unmnemonic_devices_vrs/src/routes/calls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use axum::{extract::State, response::IntoResponse};
use axum_template::{Key, RenderHtml};
use serde::{Deserialize, Serialize};
use std::env;

use openapi::apis::api20100401_call_api::{list_call, ListCallParams};
use openapi::apis::configuration::Configuration;

use crate::config::{ConfigProvider, EnvVarProvider};
use crate::AppState;

#[derive(Debug, Deserialize)]
struct TwilioCall {
from: String,
}

#[derive(Serialize)]
pub struct Calls {
calls: Vec<Call>,
}

#[derive(sqlx::FromRow, Serialize)]
pub struct Call {
from: String,
}

pub async fn get_calls(Key(key): Key, State(state): State<AppState>) -> impl IntoResponse {
// FIXME can this use a generated client?
let env_config_provider = EnvVarProvider::new(env::vars().collect());
let config = &env_config_provider.get_config();
let account_sid = config.twilio_account_sid.to_string();
let auth_token = config.twilio_auth_token.to_string();

let twilio_config = Configuration {
basic_auth: Some((account_sid.clone(), Some(auth_token))),
..Default::default()
};

let response = list_call(
&twilio_config,
ListCallParams {
account_sid: account_sid.clone(),
to: None,
from: None,
parent_call_sid: None,
status: None,
start_time: None,
start_time_less_than: None,
start_time_greater_than: None,
end_time: None,
end_time_less_than: None,
end_time_greater_than: None,
page_size: None,
page: None,
page_token: None,
},
)
.await;

match response {
Ok(result) => {
let calls = result
.calls
.unwrap()
.iter()
.map(|call| Call {
from: call.from.clone().unwrap().unwrap(),
})
.collect::<Vec<Call>>();

RenderHtml(
key.chars().skip(1).collect::<String>(),
state.engine,
Calls { calls },
)
}
Err(error) => RenderHtml(
key.chars().skip(1).collect::<String>(),
state.engine,
Calls { calls: Vec::new() },
),
};
}
2 changes: 2 additions & 0 deletions unmnemonic_devices_vrs/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod calls;
mod meetings;
mod recordings;
mod root;
mod teams;
mod util;

pub use calls::*;
pub use meetings::*;
pub use recordings::*;
pub use root::*;
Expand Down
28 changes: 28 additions & 0 deletions unmnemonic_devices_vrs/src/templates/calls.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<html>
<body>
<table>
<thead>
<tr>
<th>
from
</th>
</tr>
</thead>
<tbody>
{{#each calls}}
<tr>
<td>
{{this.from}}
</td>
</tr>
{{else}}
<tr>
<td>
no calls
</td>
</tr>
{{/each}}
</tbody>
</table>
</body>
</html>
87 changes: 87 additions & 0 deletions unmnemonic_devices_vrs/tests/api/calls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::helpers::get_with_twilio;
use select::{
document::Document,
predicate::{Descendant, Name},
};
use serde_json::json;
use speculoos::prelude::*;
use sqlx::PgPool;
use unmnemonic_devices_vrs::InjectableServices;
use wiremock::matchers::any;
use wiremock::{Mock, MockServer, ResponseTemplate};

// FIXME this isn’t an API!

#[sqlx::test()]
async fn calls_list_when_empty(db: PgPool) {
let mock_twilio = MockServer::start().await;

Mock::given(any())
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"calls": []})))
.expect(1)
.mount(&mock_twilio)
.await;

let response = get_with_twilio(
InjectableServices {
db,
twilio_address: mock_twilio.uri(),
},
"/calls",
false,
)
.await
.expect("Failed to execute request.");

assert!(response.status().is_success());
assert_eq!(
response.headers().get("Content-Type").unwrap(),
"text/html; charset=utf-8"
);

let document = Document::from(response.text().await.unwrap().as_str());
let row = document
.find(Descendant(Name("tbody"), Name("tr")))
.next()
.unwrap();

assert_that(&row.text()).contains("no calls");
}

#[sqlx::test()]
async fn calls_list_with_calls(db: PgPool) {
let mock_twilio = MockServer::start().await;

Mock::given(any())
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({"calls": [{"from": "+15145551212"}]})),
)
.expect(1)
.mount(&mock_twilio)
.await;

let response = get_with_twilio(
InjectableServices {
db,
twilio_address: mock_twilio.uri(),
},
"/calls",
false,
)
.await
.expect("Failed to execute request.");

assert!(response.status().is_success());
assert_eq!(
response.headers().get("Content-Type").unwrap(),
"text/html; charset=utf-8"
);

let document = Document::from(response.text().await.unwrap().as_str());
let row = document
.find(Descendant(Name("tbody"), Name("tr")))
.next()
.unwrap();

assert_that(&row.text()).contains("+15145551212");
}
Loading