Split project (keep core)

This commit is contained in:
Tobias Reisinger 2024-04-30 10:38:01 +02:00
parent 9bc75b9627
commit b742f0f8d6
Signed by: serguzim
GPG key ID: 13AD60C237A28DFE
127 changed files with 38 additions and 5722 deletions

188
src/app_state.rs Normal file
View file

@ -0,0 +1,188 @@
use std::collections::HashMap;
use actix::{Actor, Addr, Context, Handler, Message, Recipient};
use emgauwa_common::db::DbController;
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::models::{convert_db_list, Controller, Relay};
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RelayStates};
use futures::executor::block_on;
use sqlx::{Pool, Sqlite};
use crate::handlers::v1::ws::relays::{RelaysWs, SendRelays};
#[derive(Message)]
#[rtype(result = "Result<(), EmgauwaError>")]
pub struct DisconnectController {
pub controller_uid: EmgauwaUid,
}
#[derive(Message)]
#[rtype(result = "Result<(), EmgauwaError>")]
pub struct ConnectController {
pub address: Recipient<ControllerWsAction>,
pub controller: Controller,
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct UpdateRelayStates {
pub controller_uid: EmgauwaUid,
pub relay_states: RelayStates,
}
#[derive(Message)]
#[rtype(result = "Result<Vec<Relay>, EmgauwaError>")]
pub struct GetRelays {}
#[derive(Message)]
#[rtype(result = "Result<(), EmgauwaError>")]
pub struct Action {
pub controller_uid: EmgauwaUid,
pub action: ControllerWsAction,
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct ConnectRelayClient {
pub addr: Addr<RelaysWs>,
}
pub struct AppState {
pub pool: Pool<Sqlite>,
pub connected_controllers: HashMap<EmgauwaUid, (Controller, Recipient<ControllerWsAction>)>,
pub connected_relay_clients: Vec<Addr<RelaysWs>>,
}
impl AppState {
pub fn new(pool: Pool<Sqlite>) -> AppState {
AppState {
pool,
connected_controllers: HashMap::new(),
connected_relay_clients: Vec::new(),
}
}
fn get_relays(&self) -> Result<Vec<Relay>, EmgauwaError> {
let mut pool_conn = block_on(self.pool.acquire())?;
let db_controllers = block_on(DbController::get_all(&mut pool_conn))?;
let mut controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
self.connected_controllers
.iter()
.for_each(|(uid, (connected_controller, _))| {
if let Some(c) = controllers.iter_mut().find(|c| c.c.uid == *uid) {
c.apply_relay_states(&connected_controller.get_relay_states());
}
});
let mut relays: Vec<Relay> = Vec::new();
controllers.iter().for_each(|c| {
relays.extend(c.relays.clone());
});
Ok(relays)
}
fn notify_relay_clients(&mut self) {
self.connected_relay_clients.retain(|addr| addr.connected());
match self.get_relays() {
Ok(relays) => match serde_json::to_string(&relays) {
Ok(json) => {
self.connected_relay_clients.iter_mut().for_each(|addr| {
let relays_json = json.clone();
addr.do_send(SendRelays { relays_json });
});
}
Err(err) => {
log::error!("Failed to serialize relays: {:?}", err);
}
},
Err(err) => {
log::error!("Failed to get relays: {:?}", err);
}
};
}
}
impl Actor for AppState {
type Context = Context<Self>;
}
impl Handler<DisconnectController> for AppState {
type Result = Result<(), EmgauwaError>;
fn handle(&mut self, msg: DisconnectController, _ctx: &mut Self::Context) -> Self::Result {
let mut pool_conn = block_on(self.pool.acquire())?;
if let Some((controller, address)) = self.connected_controllers.remove(&msg.controller_uid)
{
if let Err(err) = block_on(controller.c.update_active(&mut pool_conn, false)) {
log::error!(
"Failed to mark controller {} as inactive: {:?}",
controller.c.uid,
err
);
}
// TODO: why does the block_on(send()) version not return? The AppState will be stuck.
//block_on(address.send(ControllerWsAction::Disconnect))??;
address.do_send(ControllerWsAction::Disconnect);
}
Ok(())
}
}
impl Handler<ConnectController> for AppState {
type Result = Result<(), EmgauwaError>;
fn handle(&mut self, msg: ConnectController, _ctx: &mut Self::Context) -> Self::Result {
log::debug!("Connecting controller: {}", msg.controller.c.uid);
self.connected_controllers
.insert(msg.controller.c.uid.clone(), (msg.controller, msg.address));
Ok(())
}
}
impl Handler<UpdateRelayStates> for AppState {
type Result = ();
fn handle(&mut self, msg: UpdateRelayStates, _ctx: &mut Self::Context) -> Self::Result {
if let Some((controller, _)) = self.connected_controllers.get_mut(&msg.controller_uid) {
controller.apply_relay_states(&msg.relay_states);
}
self.notify_relay_clients();
}
}
impl Handler<GetRelays> for AppState {
type Result = Result<Vec<Relay>, EmgauwaError>;
fn handle(&mut self, _msg: GetRelays, _ctx: &mut Self::Context) -> Self::Result {
self.get_relays()
}
}
impl Handler<Action> for AppState {
type Result = Result<(), EmgauwaError>;
fn handle(&mut self, msg: Action, _ctx: &mut Self::Context) -> Self::Result {
log::debug!("Forwarding action: {:?}", msg.action);
if let Some((_, address)) = self.connected_controllers.get(&msg.controller_uid) {
// TODO: why does the block_on(send()) version not return? The AppState will be stuck.
//block_on(address.send(msg.action))?
address.do_send(msg.action);
Ok(())
} else {
Err(EmgauwaError::Connection(msg.controller_uid))
}
}
}
impl Handler<ConnectRelayClient> for AppState {
type Result = ();
fn handle(&mut self, msg: ConnectRelayClient, _ctx: &mut Self::Context) -> Self::Result {
self.connected_relay_clients.push(msg.addr);
}
}

37
src/handlers/mod.rs Normal file
View file

@ -0,0 +1,37 @@
use actix_web::{error, Error, HttpRequest, HttpResponse};
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
pub mod v1;
enum EmgauwaJsonPayLoadError {
Error(error::JsonPayloadError),
}
impl Serialize for EmgauwaJsonPayLoadError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("error", 3)?;
s.serialize_field("type", "json-payload-error")?;
s.serialize_field("code", &400)?;
s.serialize_field(
"description",
&match self {
EmgauwaJsonPayLoadError::Error(err) => format!("{}", err),
},
)?;
s.end()
}
}
pub fn json_error_handler(err: error::JsonPayloadError, _: &HttpRequest) -> Error {
error::InternalError::from_response(
"",
HttpResponse::BadRequest()
.content_type("application/json")
.json(EmgauwaJsonPayLoadError::Error(err)),
)
.into()
}

View file

@ -0,0 +1,92 @@
use actix::Addr;
use actix_web::{delete, get, put, web, HttpResponse};
use emgauwa_common::db::DbController;
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
use emgauwa_common::models::{convert_db_list, Controller, FromDbModel};
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RequestControllerUpdate};
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
#[get("/controllers")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let db_controllers = DbController::get_all(&mut pool_conn).await?;
let controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
Ok(HttpResponse::Ok().json(controllers))
}
#[get("/controllers/{controller_id}")]
pub async fn show(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
Ok(HttpResponse::Ok().json(return_controller))
}
#[put("/controllers/{controller_id}")]
pub async fn update(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
path: web::Path<(String,)>,
data: web::Json<RequestControllerUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let controller = controller
.update(&mut pool_conn, data.name.as_str(), controller.relay_count)
.await?;
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
app_state
.send(app_state::Action {
controller_uid: uid.clone(),
action: ControllerWsAction::Controller(return_controller.clone()),
})
.await??;
Ok(HttpResponse::Ok().json(return_controller))
}
#[delete("/controllers/{controller_id}")]
pub async fn delete(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
app_state
.send(app_state::DisconnectController {
controller_uid: uid.clone(),
})
.await??;
DbController::delete_by_uid(&mut pool_conn, uid).await?;
Ok(HttpResponse::Ok().json("controller got deleted"))
}

161
src/handlers/v1/macros.rs Normal file
View file

@ -0,0 +1,161 @@
use actix::Addr;
use actix_web::{delete, get, post, put, web, HttpResponse};
use emgauwa_common::db::DbMacro;
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
use emgauwa_common::models::{convert_db_list, FromDbModel, Macro, MacroAction, Relay};
use emgauwa_common::types::{
ControllerWsAction, EmgauwaUid, RequestMacroCreate, RequestMacroExecute, RequestMacroUpdate,
};
use itertools::Itertools;
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
#[get("/macros")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let db_macros = DbMacro::get_all(&mut pool_conn).await?;
let macros: Vec<Macro> = convert_db_list(&mut pool_conn, db_macros)?;
Ok(HttpResponse::Ok().json(macros))
}
#[get("/macros/{macro_id}")]
pub async fn show(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (macro_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
Ok(HttpResponse::Ok().json(return_macro))
}
#[post("/macros")]
pub async fn add(
pool: web::Data<Pool<Sqlite>>,
data: web::Json<RequestMacroCreate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let new_macro = DbMacro::create(&mut pool_conn, EmgauwaUid::default(), &data.name).await?;
new_macro
.set_actions(&mut pool_conn, data.actions.as_slice())
.await?;
let return_macro = Macro::from_db_model(&mut pool_conn, new_macro)?;
Ok(HttpResponse::Created().json(return_macro))
}
#[put("/macros/{macro_id}")]
pub async fn update(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
data: web::Json<RequestMacroUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (macro_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
if let Some(name) = &data.name {
db_macro.update(&mut pool_conn, name).await?;
}
if let Some(actions) = &data.actions {
db_macro
.set_actions(&mut pool_conn, actions.as_slice())
.await?;
}
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
Ok(HttpResponse::Ok().json(return_macro))
}
#[delete("/macros/{macro_id}")]
pub async fn delete(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (macro_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
DbMacro::delete_by_uid(&mut pool_conn, uid).await?;
Ok(HttpResponse::Ok().json("macro got deleted"))
}
#[put("/macros/{macro_id}/execute")]
pub async fn execute(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
path: web::Path<(String,)>,
query: web::Query<RequestMacroExecute>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (macro_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let actions_db = match query.weekday {
None => db_macro.get_actions(&mut pool_conn).await?,
Some(weekday) => {
db_macro
.get_actions_weekday(&mut pool_conn, weekday)
.await?
}
};
let mut actions: Vec<MacroAction> = convert_db_list(&mut pool_conn, actions_db)?;
for action in &actions {
action.execute(&mut pool_conn).await?;
}
let affected_controller_uids: Vec<EmgauwaUid> = actions
.iter()
.map(|action| action.relay.controller_id.clone())
.unique()
.collect();
for controller_uid in affected_controller_uids {
let mut affected_relays: Vec<Relay> = Vec::new();
let mut affected_relay_ids: Vec<i64> = Vec::new();
for action in actions.iter_mut() {
if affected_relay_ids.contains(&action.relay.r.id) {
continue;
}
action.relay.reload(&mut pool_conn)?;
affected_relays.push(action.relay.clone());
affected_relay_ids.push(action.relay.r.id);
}
app_state
.send(app_state::Action {
controller_uid,
action: ControllerWsAction::Relays(affected_relays.clone()),
})
.await??;
}
Ok(HttpResponse::Ok().finish()) // TODO add a message?
}

6
src/handlers/v1/mod.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod controllers;
pub mod macros;
pub mod relays;
pub mod schedules;
pub mod tags;
pub mod ws;

185
src/handlers/v1/relays.rs Normal file
View file

@ -0,0 +1,185 @@
use actix::Addr;
use actix_web::{get, post, put, web, HttpResponse};
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbTag};
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
use emgauwa_common::models::{convert_db_list, FromDbModel, Relay};
use emgauwa_common::types::{
ControllerWsAction, EmgauwaUid, RequestRelayPulse, RequestRelayUpdate,
};
use emgauwa_common::utils;
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
#[get("/relays")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let db_relays = DbRelay::get_all(&mut pool_conn).await?;
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
Ok(HttpResponse::Ok().json(relays))
}
#[get("/relays/tag/{tag}")]
pub async fn tagged(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (tag,) = path.into_inner();
let tag_db = DbTag::get_by_tag(&mut pool_conn, &tag)
.await?
.ok_or(DatabaseError::NotFound)?;
let db_relays = DbRelay::get_by_tag(&mut pool_conn, &tag_db).await?;
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
Ok(HttpResponse::Ok().json(relays))
}
#[get("/controllers/{controller_id}/relays")]
pub async fn index_for_controller(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid,) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let db_relays = controller.get_relays(&mut pool_conn).await?;
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
Ok(HttpResponse::Ok().json(relays))
}
#[get("/controllers/{controller_id}/relays/{relay_num}")]
pub async fn show_for_controller(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String, i64)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid, relay_num) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
.await?
.ok_or(DatabaseError::NotFound)?;
let return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
Ok(HttpResponse::Ok().json(return_relay))
}
#[put("/controllers/{controller_id}/relays/{relay_num}")]
pub async fn update_for_controller(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
path: web::Path<(String, i64)>,
data: web::Json<RequestRelayUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid, relay_num) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let mut relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
.await?
.ok_or(DatabaseError::NotFound)?;
if let Some(name) = &data.name {
relay = relay.update(&mut pool_conn, name.as_str()).await?;
}
if let Some(schedule_uids) = &data.schedules {
if schedule_uids.len() == 7 {
let mut schedules = Vec::new();
for s_uid in schedule_uids {
schedules.push(s_uid.get_schedule(&mut pool_conn).await?);
}
DbJunctionRelaySchedule::set_schedules(
&mut pool_conn,
&relay,
schedules.iter().collect(),
)
.await?;
}
}
if let Some(s_uid) = &data.active_schedule {
let schedule = s_uid.get_schedule(&mut pool_conn).await?;
DbJunctionRelaySchedule::set_schedule(
&mut pool_conn,
&relay,
&schedule,
utils::get_weekday(),
)
.await?;
}
if let Some(tags) = &data.tags {
relay.set_tags(&mut pool_conn, tags.as_slice()).await?;
}
let relay = relay.reload(&mut pool_conn).await?;
let return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
app_state
.send(app_state::Action {
controller_uid: uid,
action: ControllerWsAction::Relays(vec![return_relay.clone()]),
})
.await??;
Ok(HttpResponse::Ok().json(return_relay))
}
#[post("/controllers/{controller_id}/relays/{relay_num}/pulse")]
pub async fn pulse(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
path: web::Path<(String, i64)>,
data: web::Json<RequestRelayPulse>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (controller_uid, relay_num) = path.into_inner();
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
.await?
.ok_or(DatabaseError::NotFound)?;
let duration = data.duration.filter(|&d| d > 0);
app_state
.send(app_state::Action {
controller_uid: uid,
action: ControllerWsAction::RelayPulse((relay.number, duration)),
})
.await??;
Ok(HttpResponse::Ok().finish()) // TODO add a message?
}

View file

@ -0,0 +1,196 @@
use actix::Addr;
use actix_web::{delete, get, post, put, web, HttpResponse};
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbSchedule, DbTag};
use emgauwa_common::errors::{ApiError, DatabaseError, EmgauwaError};
use emgauwa_common::models::{convert_db_list, FromDbModel, Schedule};
use emgauwa_common::types::{
ControllerWsAction, RequestScheduleCreate, RequestScheduleUpdate, ScheduleUid,
};
use itertools::Itertools;
use sqlx::pool::PoolConnection;
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
#[get("/schedules")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let db_schedules = DbSchedule::get_all(&mut pool_conn).await?;
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
Ok(HttpResponse::Ok().json(schedules))
}
#[get("/schedules/tag/{tag}")]
pub async fn tagged(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (tag,) = path.into_inner();
let tag_db = DbTag::get_by_tag(&mut pool_conn, &tag)
.await?
.ok_or(DatabaseError::NotFound)?;
let db_schedules = DbSchedule::get_by_tag(&mut pool_conn, &tag_db).await?;
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
Ok(HttpResponse::Ok().json(schedules))
}
#[get("/schedules/{schedule_id}")]
pub async fn show(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (schedule_uid,) = path.into_inner();
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
let schedule = DbSchedule::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
Ok(HttpResponse::Ok().json(return_schedule))
}
#[post("/schedules")]
pub async fn add(
pool: web::Data<Pool<Sqlite>>,
data: web::Json<RequestScheduleCreate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let new_schedule = DbSchedule::create(
&mut pool_conn,
ScheduleUid::default(),
&data.name,
&data.periods,
)
.await?;
if let Some(tags) = &data.tags {
new_schedule
.set_tags(&mut pool_conn, tags.as_slice())
.await?;
}
let return_schedule = Schedule::from_db_model(&mut pool_conn, new_schedule)?;
Ok(HttpResponse::Created().json(return_schedule))
}
async fn add_list_single(
conn: &mut PoolConnection<Sqlite>,
request_schedule: &RequestScheduleCreate,
) -> Result<DbSchedule, DatabaseError> {
let new_schedule = DbSchedule::create(
conn,
ScheduleUid::default(),
&request_schedule.name,
&request_schedule.periods,
)
.await?;
if let Some(tags) = &request_schedule.tags {
new_schedule.set_tags(conn, tags.as_slice()).await?;
}
Ok(new_schedule)
}
#[post("/schedules/list")]
pub async fn add_list(
pool: web::Data<Pool<Sqlite>>,
data: web::Json<Vec<RequestScheduleCreate>>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let mut db_schedules: Vec<DbSchedule> = Vec::new();
for s in data.iter() {
let new_s = futures::executor::block_on(add_list_single(&mut pool_conn, s))?;
db_schedules.push(new_s);
}
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
Ok(HttpResponse::Created().json(schedules))
}
#[put("/schedules/{schedule_id}")]
pub async fn update(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
path: web::Path<(String,)>,
data: web::Json<RequestScheduleUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (schedule_uid,) = path.into_inner();
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
let schedule = DbSchedule::get_by_uid(&mut pool_conn, &uid)
.await?
.ok_or(DatabaseError::NotFound)?;
let name = match &data.name {
None => schedule.name.as_str(),
Some(name) => name.as_str(),
};
let periods = match &data.periods {
None => &schedule.periods,
Some(period) => period,
};
let schedule = schedule.update(&mut pool_conn, name, periods).await?;
if let Some(tags) = &data.tags {
schedule.set_tags(&mut pool_conn, tags.as_slice()).await?;
}
let controller_ids: Vec<i64> = DbJunctionRelaySchedule::get_relays(&mut pool_conn, &schedule)
.await?
.into_iter()
.map(|r| r.controller_id)
.unique()
.collect();
for controller_id in controller_ids {
let controller = DbController::get(&mut pool_conn, controller_id)
.await?
.ok_or(DatabaseError::NotFound)?;
app_state
.send(app_state::Action {
controller_uid: controller.uid,
action: ControllerWsAction::Schedules(vec![schedule.clone()]),
})
.await??;
}
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
Ok(HttpResponse::Ok().json(return_schedule))
}
#[delete("/schedules/{schedule_id}")]
pub async fn delete(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (schedule_uid,) = path.into_inner();
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
match uid {
ScheduleUid::Off => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
ScheduleUid::On => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
ScheduleUid::Any(_) => {
DbSchedule::delete_by_uid(&mut pool_conn, uid).await?;
Ok(HttpResponse::Ok().json("schedule got deleted"))
}
}
}

61
src/handlers/v1/tags.rs Normal file
View file

@ -0,0 +1,61 @@
use actix_web::{delete, get, post, web, HttpResponse};
use emgauwa_common::db::DbTag;
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
use emgauwa_common::models::{FromDbModel, Tag};
use emgauwa_common::types::RequestTagCreate;
use sqlx::{Pool, Sqlite};
#[get("/tags")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let db_tags = DbTag::get_all(&mut pool_conn).await?;
let tags: Vec<String> = db_tags.iter().map(|t| t.tag.clone()).collect();
Ok(HttpResponse::Ok().json(tags))
}
#[get("/tags/{tag_name}")]
pub async fn show(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (tag_name,) = path.into_inner();
let tag = DbTag::get_by_tag(&mut pool_conn, &tag_name)
.await?
.ok_or(DatabaseError::NotFound)?;
let return_tag = Tag::from_db_model(&mut pool_conn, tag)?;
Ok(HttpResponse::Ok().json(return_tag))
}
#[delete("/tags/{tag_name}")]
pub async fn delete(
pool: web::Data<Pool<Sqlite>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let (tag_name,) = path.into_inner();
DbTag::delete_by_tag(&mut pool_conn, &tag_name).await?;
Ok(HttpResponse::Ok().json("tag got deleted"))
}
#[post("/tags")]
pub async fn add(
pool: web::Data<Pool<Sqlite>>,
data: web::Json<RequestTagCreate>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let new_tag = DbTag::create(&mut pool_conn, &data.tag).await?;
let cache = (Vec::new(), Vec::new()); // a new tag can't have any relays or schedules
let return_tag = Tag::from_db_model_cache(&mut pool_conn, new_tag, cache)?;
Ok(HttpResponse::Created().json(return_tag))
}

View file

@ -0,0 +1,114 @@
use actix::{Actor, AsyncContext};
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
use emgauwa_common::models::{Controller, FromDbModel};
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RelayStates};
use emgauwa_common::utils;
use futures::executor::block_on;
use sqlx::pool::PoolConnection;
use sqlx::Sqlite;
use crate::app_state::{Action, ConnectController, UpdateRelayStates};
use crate::handlers::v1::ws::controllers::ControllersWs;
impl ControllersWs {
pub fn handle_register(
&mut self,
conn: &mut PoolConnection<Sqlite>,
ctx: &mut <ControllersWs as Actor>::Context,
controller: Controller,
) -> Result<(), EmgauwaError> {
log::info!(
"Registering controller: {} ({})",
controller.c.name,
controller.c.uid
);
let c = &controller.c;
let controller_db = block_on(DbController::get_by_uid_or_create(
conn,
&c.uid,
&c.name,
c.relay_count,
))?;
block_on(controller_db.update_active(conn, true))?;
// update only the relay count
block_on(controller_db.update(conn, &controller_db.name, c.relay_count))?;
for relay in &controller.relays {
log::debug!(
"Registering relay: {} ({})",
relay.r.name,
match relay.is_on {
Some(true) => "+",
Some(false) => "-",
None => "?",
}
);
let (new_relay, created) = block_on(DbRelay::get_by_controller_and_num_or_create(
conn,
&controller_db,
relay.r.number,
&relay.r.name,
))?;
if created {
let mut relay_schedules = Vec::new();
for schedule in &relay.schedules {
let (new_schedule, _) = block_on(DbSchedule::get_by_uid_or_create(
conn,
schedule.uid.clone(),
&schedule.name,
&schedule.periods,
))?;
relay_schedules.push(new_schedule);
}
block_on(DbJunctionRelaySchedule::set_schedules(
conn,
&new_relay,
relay_schedules.iter().collect(),
))?;
}
}
let controller_uid = &controller.c.uid;
let controller_db = block_on(DbController::get_by_uid(conn, controller_uid))?
.ok_or(DatabaseError::InsertGetError)?;
let controller = Controller::from_db_model(conn, controller_db)?;
let addr = ctx.address();
self.controller_uid = Some(controller_uid.clone());
block_on(self.app_state.send(ConnectController {
address: addr.recipient(),
controller: controller.clone(),
}))??;
block_on(self.app_state.send(Action {
controller_uid: controller_uid.clone(),
action: ControllerWsAction::Controller(controller.clone()),
}))??;
block_on(self.app_state.send(Action {
controller_uid: controller_uid.clone(),
action: ControllerWsAction::Relays(controller.relays),
}))??;
log::debug!("Done registering controller");
Ok(())
}
pub fn handle_relay_states(
&mut self,
controller_uid: EmgauwaUid,
relay_states: RelayStates,
) -> Result<(), EmgauwaError> {
log::debug!(
"Received relay states: {} for {}",
utils::printable_relay_states(&relay_states),
controller_uid
);
block_on(self.app_state.send(UpdateRelayStates {
controller_uid,
relay_states,
}))?;
Ok(())
}
}

View file

@ -0,0 +1,154 @@
mod handlers;
use std::time::Instant;
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
use actix_web_actors::ws;
use actix_web_actors::ws::ProtocolError;
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid};
use futures::executor::block_on;
use sqlx::pool::PoolConnection;
use sqlx::{Pool, Sqlite};
use ws::Message;
use crate::app_state::{AppState, DisconnectController};
use crate::utils::flatten_result;
pub struct ControllersWs {
pub pool: Pool<Sqlite>,
pub controller_uid: Option<EmgauwaUid>,
pub app_state: Addr<AppState>,
pub hb: Instant,
}
impl Actor for ControllersWs {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
self.hb(ctx);
}
fn stopped(&mut self, _ctx: &mut Self::Context) {
if let Some(controller_uid) = &self.controller_uid {
let flat_res = flatten_result(
block_on(self.app_state.send(DisconnectController {
controller_uid: controller_uid.clone(),
}))
.map_err(EmgauwaError::from),
);
if let Err(err) = flat_res {
log::error!("Error disconnecting controller: {:?}", err);
}
}
}
}
impl ControllersWs {
pub fn handle_action(
&mut self,
conn: &mut PoolConnection<Sqlite>,
ctx: &mut <ControllersWs as Actor>::Context,
action: ControllerWsAction,
) {
let action_res = match action {
ControllerWsAction::Register(controller) => self.handle_register(conn, ctx, controller),
ControllerWsAction::RelayStates((controller_uid, relay_states)) => {
self.handle_relay_states(controller_uid, relay_states)
}
_ => Ok(()),
};
if let Err(e) = action_res {
log::error!("Error handling action: {:?}", e);
ctx.text(
serde_json::to_string(&e).unwrap_or(format!("Error in handling action: {:?}", e)),
);
}
}
// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
// check client heartbeats
if Instant::now().duration_since(act.hb) > HEARTBEAT_TIMEOUT {
log::warn!("Websocket Controller heartbeat failed, disconnecting!");
ctx.stop();
// don't try to send a ping
return;
}
ctx.ping(&[]);
});
}
}
impl Handler<ControllerWsAction> for ControllersWs {
type Result = Result<(), EmgauwaError>;
fn handle(&mut self, action: ControllerWsAction, ctx: &mut Self::Context) -> Self::Result {
match action {
ControllerWsAction::Disconnect => {
ctx.close(None);
ctx.stop();
}
_ => {
let action_json = serde_json::to_string(&action)?;
ctx.text(action_json);
}
}
Ok(())
}
}
impl StreamHandler<Result<Message, ProtocolError>> for ControllersWs {
fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
let mut pool_conn = match block_on(self.pool.acquire()) {
Ok(conn) => conn,
Err(err) => {
log::error!("Failed to acquire database connection: {:?}", err);
ctx.stop();
return;
}
};
let msg = match msg {
Err(_) => {
ctx.stop();
return;
}
Ok(msg) => msg,
};
match msg {
Message::Ping(msg) => {
self.hb = Instant::now();
ctx.pong(&msg)
}
Message::Pong(_) => {
self.hb = Instant::now();
}
Message::Text(text) => match serde_json::from_str(&text) {
Ok(action) => {
self.handle_action(&mut pool_conn, ctx, action);
}
Err(e) => {
log::error!("Error deserializing action: {:?}", e);
ctx.text(
serde_json::to_string(&EmgauwaError::Serialization(e))
.unwrap_or(String::from("Error in deserializing action")),
);
}
},
Message::Binary(_) => log::warn!("Received unexpected binary in controller ws"),
Message::Close(reason) => {
ctx.close(reason);
ctx.stop();
}
Message::Continuation(_) => {
ctx.stop();
}
Message::Nop => (),
}
}
}

53
src/handlers/v1/ws/mod.rs Normal file
View file

@ -0,0 +1,53 @@
use std::time::Instant;
use actix::Addr;
use actix_web::{get, web, HttpRequest, HttpResponse};
use actix_web_actors::ws;
use emgauwa_common::errors::EmgauwaError;
use sqlx::{Pool, Sqlite};
use crate::app_state::AppState;
use crate::handlers::v1::ws::controllers::ControllersWs;
use crate::handlers::v1::ws::relays::RelaysWs;
pub mod controllers;
pub mod relays;
#[get("/ws/controllers")]
pub async fn ws_controllers(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
req: HttpRequest,
stream: web::Payload,
) -> Result<HttpResponse, EmgauwaError> {
let resp = ws::start(
ControllersWs {
pool: pool.get_ref().clone(),
controller_uid: None,
app_state: app_state.get_ref().clone(),
hb: Instant::now(),
},
&req,
stream,
)
.map_err(|_| EmgauwaError::Internal(String::from("error starting websocket")));
resp
}
#[get("/ws/relays")]
pub async fn ws_relays(
app_state: web::Data<Addr<AppState>>,
req: HttpRequest,
stream: web::Payload,
) -> Result<HttpResponse, EmgauwaError> {
let resp = ws::start(
RelaysWs {
app_state: app_state.get_ref().clone(),
hb: Instant::now(),
},
&req,
stream,
)
.map_err(|_| EmgauwaError::Internal(String::from("error starting websocket")));
resp
}

View file

@ -0,0 +1,106 @@
use std::time::Instant;
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, StreamHandler};
use actix_web_actors::ws;
use actix_web_actors::ws::ProtocolError;
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
use emgauwa_common::errors::EmgauwaError;
use futures::executor::block_on;
use crate::app_state::{AppState, ConnectRelayClient};
pub struct RelaysWs {
pub app_state: Addr<AppState>,
pub hb: Instant,
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct SendRelays {
pub relays_json: String,
}
impl Actor for RelaysWs {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
// get unique id for ctx
match self.get_relays_json() {
Ok(relays_json) => {
ctx.text(relays_json);
self.hb(ctx);
block_on(self.app_state.send(ConnectRelayClient {
addr: ctx.address(),
}))
.unwrap();
}
Err(err) => {
log::error!("Error getting relays: {:?}", err);
ctx.stop();
}
}
}
}
impl RelaysWs {
fn get_relays_json(&self) -> Result<String, EmgauwaError> {
let relays = block_on(self.app_state.send(crate::app_state::GetRelays {}))??;
serde_json::to_string(&relays).map_err(EmgauwaError::from)
}
// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
// check client heartbeats
if Instant::now().duration_since(act.hb) > HEARTBEAT_TIMEOUT {
log::debug!("Websocket Relay heartbeat failed, disconnecting!");
ctx.stop();
// don't try to send a ping
return;
}
ctx.ping(&[]);
});
}
}
impl StreamHandler<Result<ws::Message, ProtocolError>> for RelaysWs {
fn handle(&mut self, msg: Result<ws::Message, ProtocolError>, ctx: &mut Self::Context) {
let msg = match msg {
Err(_) => {
ctx.stop();
return;
}
Ok(msg) => msg,
};
match msg {
ws::Message::Ping(msg) => {
self.hb = Instant::now();
ctx.pong(&msg)
}
ws::Message::Pong(_) => {
self.hb = Instant::now();
}
ws::Message::Text(_) => log::debug!("Received unexpected text in relays ws"),
ws::Message::Binary(_) => log::debug!("Received unexpected binary in relays ws"),
ws::Message::Close(reason) => {
ctx.close(reason);
ctx.stop();
}
ws::Message::Continuation(_) => {
ctx.stop();
}
ws::Message::Nop => (),
}
}
}
impl Handler<SendRelays> for RelaysWs {
type Result = ();
fn handle(&mut self, msg: SendRelays, ctx: &mut Self::Context) -> Self::Result {
ctx.text(msg.relays_json);
}
}

124
src/main.rs Normal file
View file

@ -0,0 +1,124 @@
use std::net::TcpListener;
use actix::{Actor, Arbiter};
use actix_cors::Cors;
use actix_web::middleware::TrailingSlash;
use actix_web::{middleware, web, App, HttpServer};
use emgauwa_common::db::DbController;
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::utils::{drop_privileges, init_logging};
use serde_json::json;
use utoipa_swagger_ui::SwaggerUi;
use crate::app_state::AppState;
mod app_state;
mod handlers;
mod settings;
mod utils;
#[actix_web::main]
async fn main() -> Result<(), std::io::Error> {
let settings = settings::init()?;
let listener = TcpListener::bind(format!("{}:{}", settings.server.host, settings.server.port))?;
drop_privileges(&settings.permissions)?;
init_logging(&settings.logging.level)?;
let pool = emgauwa_common::db::init(&settings.database).await?;
let mut conn = pool.acquire().await.map_err(EmgauwaError::from)?;
DbController::all_inactive(&mut conn)
.await
.map_err(EmgauwaError::from)?;
conn.close().await.map_err(EmgauwaError::from)?;
let app_state_arbiter = Arbiter::with_tokio_rt(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap()
});
let app_state_pool = pool.clone();
let app_state = Actor::start_in_arbiter(&app_state_arbiter.handle(), move |_| {
AppState::new(app_state_pool)
});
log::info!(
"Starting server on {}:{}",
settings.server.host,
settings.server.port
);
HttpServer::new(move || {
let cors = Cors::default().allow_any_method().allow_any_header();
let origins = settings.origins.clone();
let cors = match settings.origins.is_empty() {
true => cors.allow_any_origin(),
false => cors.allowed_origin_fn(move |origin, _req_head| {
origins.contains(&origin.to_str().unwrap_or_default().to_string())
}),
};
let api_default = json!({
"openapi": "3.0.0",
"info": {
"version": "0.0.0",
"title": "Failed to load API documentation",
}
});
let api_v1_json =
serde_json::from_str(include_str!(concat!(env!("OUT_DIR"), "/api.v1.json")))
.unwrap_or(api_default.clone());
App::new()
.wrap(cors)
.wrap(middleware::Logger::default())
.app_data(web::JsonConfig::default().error_handler(handlers::json_error_handler))
.app_data(web::Data::new(pool.clone()))
.app_data(web::Data::new(app_state.clone()))
.service(
SwaggerUi::new("/api/docs/{_:.*}")
.external_urls_from_iter_unchecked([("/api/v1.json", api_v1_json)]),
)
.service(
web::scope("/api/v1")
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
.service(handlers::v1::controllers::index)
.service(handlers::v1::controllers::show)
.service(handlers::v1::controllers::update)
.service(handlers::v1::controllers::delete)
.service(handlers::v1::relays::index)
.service(handlers::v1::relays::tagged)
.service(handlers::v1::relays::index_for_controller)
.service(handlers::v1::relays::show_for_controller)
.service(handlers::v1::relays::update_for_controller)
.service(handlers::v1::relays::pulse)
.service(handlers::v1::schedules::index)
.service(handlers::v1::schedules::tagged)
.service(handlers::v1::schedules::show)
.service(handlers::v1::schedules::add)
.service(handlers::v1::schedules::add_list)
.service(handlers::v1::schedules::update)
.service(handlers::v1::schedules::delete)
.service(handlers::v1::tags::index)
.service(handlers::v1::tags::show)
.service(handlers::v1::tags::delete)
.service(handlers::v1::tags::add)
.service(handlers::v1::macros::index)
.service(handlers::v1::macros::show)
.service(handlers::v1::macros::add)
.service(handlers::v1::macros::update)
.service(handlers::v1::macros::delete)
.service(handlers::v1::macros::execute)
.service(handlers::v1::ws::ws_controllers)
.service(handlers::v1::ws::ws_relays),
)
})
.listen(listener)?
.run()
.await
}

32
src/settings.rs Normal file
View file

@ -0,0 +1,32 @@
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::settings;
use serde_derive::Deserialize;
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Settings {
pub server: settings::Server,
pub database: String,
pub permissions: settings::Permissions,
pub logging: settings::Logging,
pub origins: Vec<String>,
}
impl Default for Settings {
fn default() -> Self {
Settings {
server: settings::Server::default(),
database: String::from("sqlite://emgauwa-core.sqlite"),
permissions: settings::Permissions::default(),
logging: settings::Logging::default(),
origins: Vec::new(),
}
}
}
pub fn init() -> Result<Settings, EmgauwaError> {
settings::load("core", "CORE")
}

7
src/utils.rs Normal file
View file

@ -0,0 +1,7 @@
pub fn flatten_result<T, E>(res: Result<Result<T, E>, E>) -> Result<T, E> {
match res {
Ok(Ok(t)) => Ok(t),
Ok(Err(e)) => Err(e),
Err(e) => Err(e),
}
}