Buttplug sex toy control library

chore: Add error handling and more functions to REST server

+187 -25
+2
crates/intiface_engine/Cargo.toml
··· 65 65 tokio-stream = "0.1.17" 66 66 dashmap = "6.1.0" 67 67 axum = "0.8.4" 68 + anyhow = "1.0.98" 69 + strum = { version = "0.27.2", features = ["derive"] } 68 70 69 71 [build-dependencies] 70 72 vergen-gitcl = {version = "1.0.8", features = ["build"]}
+185 -25
crates/intiface_engine/src/rest_server.rs
··· 1 - use std::{collections::BTreeMap, io, net::SocketAddr, sync::Arc}; 1 + use std::{collections::BTreeMap, io, net::SocketAddr, process::Output, str::FromStr, sync::Arc}; 2 2 3 - use axum::{extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router}; 4 - use buttplug_client::{ButtplugClient, ButtplugClientDevice}; 3 + use anyhow::Context; 4 + use axum::{ 5 + Json, Router, 6 + extract::{Path, State, rejection::JsonRejection}, 7 + http::StatusCode, 8 + response::{IntoResponse, Response}, 9 + routing::{get, put}, 10 + }; 11 + use buttplug_client::{device::ClientDeviceOutputCommand, ButtplugClient, ButtplugClientDevice, ButtplugClientError}; 5 12 use buttplug_client_in_process::ButtplugInProcessClientConnectorBuilder; 6 - use buttplug_core::message::DeviceFeature; 13 + use buttplug_core::message::{DeviceFeature, OutputType}; 7 14 use buttplug_server::ButtplugServer; 15 + use serde::Serialize; 16 + use strum::IntoEnumIterator; 17 + use thiserror::Error; 8 18 use tokio::net::TcpListener; 9 - use serde::{Serialize, Deserialize}; 19 + 20 + #[derive(Error, Debug)] 21 + enum IntifaceRestError { 22 + #[error("JsonRejection: {0}")] 23 + JsonRejection(JsonRejection), 24 + #[error("Library Error: {0}")] 25 + ButtplugClientError(ButtplugClientError), 26 + #[error("Device index {0} does not refer to a currently connected device.")] 27 + InvalidDevice(u32), 28 + #[error("Device index {0} feature index {1} does not refer to a valid device feature.")] 29 + InvalidFeature(u32, u32), 30 + #[error("{0} is not a valid output type. Valid output types are: {1:?}")] 31 + InvalidOutputType(String, Vec<OutputType>), 32 + #[error("{0} is not a valid input type. Valid input types are: {1:?}")] 33 + InvalidInputType(String, Vec<String>), 34 + #[error("{0} is not a valid input commands. Valid input commands are: {1:?})")] 35 + InvalidInputCommand(u32, Vec<String>), 36 + #[error("Value {0} is not valid for the current command.)")] 37 + InvalidValue(u32), 38 + } 39 + 40 + // Tell axum how `AppError` should be converted into a response. 41 + // 42 + // This is also a convenient place to log errors. 43 + impl IntoResponse for IntifaceRestError { 44 + fn into_response(self) -> Response { 45 + let (status, message) = match self { 46 + IntifaceRestError::JsonRejection(rejection) => { 47 + // This error is caused by bad user input so don't log it 48 + (rejection.status(), rejection.body_text()) 49 + } 50 + _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), 51 + }; 52 + (status, message).into_response() 53 + } 54 + } 55 + 56 + impl From<JsonRejection> for IntifaceRestError { 57 + fn from(rejection: JsonRejection) -> Self { 58 + Self::JsonRejection(rejection) 59 + } 60 + } 61 + 62 + impl From<ButtplugClientError> for IntifaceRestError { 63 + fn from(error: ButtplugClientError) -> Self { 64 + Self::ButtplugClientError(error) 65 + } 66 + } 10 67 11 68 #[derive(Serialize)] 12 69 struct IntifaceRestDevice { 13 70 index: u32, 14 71 name: String, 15 72 display_name: Option<String>, 16 - features: BTreeMap<u32, DeviceFeature> 73 + features: BTreeMap<u32, DeviceFeature>, 17 74 } 18 75 19 - impl IntifaceRestDevice { 20 - fn from_client_device(device: &ButtplugClientDevice) -> Self { 76 + impl From<&ButtplugClientDevice> for IntifaceRestDevice { 77 + fn from(device: &ButtplugClientDevice) -> Self { 21 78 Self { 22 79 index: device.index(), 23 80 name: device.name().clone(), 24 81 display_name: device.display_name().clone(), 25 - features: device.device_features() 82 + features: device 83 + .device_features() 26 84 .iter() 27 85 .map(|(i, d)| (*i, d.feature().clone())) 28 - .collect() 86 + .collect(), 29 87 } 30 88 } 31 89 } ··· 40 98 client.stop_scanning().await; 41 99 } 42 100 43 - async fn set_device_vibrate_speed(State(client): State<Arc<ButtplugClient>>, Path((index, level)): Path<(u32, f64)>) -> Result<(), StatusCode> { 44 - client 45 - .devices() 46 - .get(&index) 47 - .ok_or(StatusCode::NOT_FOUND)? 48 - .vibrate(level) 49 - .await 50 - .map_err(|e| StatusCode::UNPROCESSABLE_ENTITY) 101 + async fn stop_all_devices(State(client): State<Arc<ButtplugClient>>) { 102 + client.stop_all_devices().await; 51 103 } 52 104 53 - async fn get_devices(State(client): State<Arc<ButtplugClient>>) -> Json<BTreeMap<u32, IntifaceRestDevice>> { 105 + async fn stop_device( 106 + State(client): State<Arc<ButtplugClient>>, 107 + Path(index): Path<u32>, 108 + ) -> Result<(), IntifaceRestError> { 109 + Ok( 110 + client 111 + .devices() 112 + .get(&index) 113 + .ok_or(IntifaceRestError::InvalidDevice(index))? 114 + .stop() 115 + .await 116 + .map_err(|e| IntifaceRestError::ButtplugClientError(e))?, 117 + ) 118 + } 119 + 120 + async fn set_device_output( 121 + State(client): State<Arc<ButtplugClient>>, 122 + Path((index, command, level)): Path<(u32, String, f64)>, 123 + ) -> Result<(), IntifaceRestError> { 124 + let command_type = OutputType::from_str(&command).map_err(|_| 125 + IntifaceRestError::InvalidOutputType( 126 + command, 127 + OutputType::iter().collect::<Vec<OutputType>>() 128 + ) 129 + )?; 130 + 131 + Ok(()) 132 + /* 133 + let cmd = ClientDeviceOutputCommand:: 134 + 135 + Ok(client 136 + .devices() 137 + .get(&index) 138 + .ok_or(IntifaceRestError::InvalidDevice(index))? 139 + .send_command(client_device_command) 140 + .await 141 + .map_err(|e| IntifaceRestError::ButtplugClientError(e))?) 142 + */ 143 + } 144 + 145 + async fn get_devices( 146 + State(client): State<Arc<ButtplugClient>>, 147 + ) -> Json<BTreeMap<u32, IntifaceRestDevice>> { 54 148 client 55 - .devices() 56 - .iter() 57 - .map(|(i, x)| (*i, IntifaceRestDevice::from_client_device(x))) 58 - .collect::<BTreeMap<u32, IntifaceRestDevice>>() 59 - .into() 149 + .devices() 150 + .iter() 151 + .map(|(i, x)| (*i, x.into())) 152 + .collect::<BTreeMap<u32, IntifaceRestDevice>>() 153 + .into() 154 + } 155 + 156 + async fn get_device( 157 + State(client): State<Arc<ButtplugClient>>, 158 + Path(index): Path<u32>, 159 + ) -> Result<Json<IntifaceRestDevice>, IntifaceRestError> { 160 + Ok(IntifaceRestDevice::from(client.devices().get(&index).ok_or(IntifaceRestError::InvalidDevice(index))?).into()) 161 + } 162 + 163 + async fn get_features( 164 + State(client): State<Arc<ButtplugClient>>, 165 + Path(index): Path<u32>, 166 + ) -> Result<Json<BTreeMap<u32, DeviceFeature>>, IntifaceRestError> { 167 + Ok( 168 + client 169 + .devices() 170 + .get(&index) 171 + .ok_or(IntifaceRestError::InvalidDevice(index))? 172 + .device_features() 173 + .iter() 174 + .map(|(i, f)| (*i, f.feature().clone())) 175 + .collect::<BTreeMap<u32, DeviceFeature>>() 176 + .into(), 177 + ) 178 + } 179 + 180 + async fn get_feature( 181 + State(client): State<Arc<ButtplugClient>>, 182 + Path((index, feature_index)): Path<(u32, u32)>, 183 + ) -> Result<Json<DeviceFeature>, IntifaceRestError> { 184 + Ok( 185 + client 186 + .devices() 187 + .get(&index) 188 + .ok_or(IntifaceRestError::InvalidDevice(index))? 189 + .device_features() 190 + .get(&feature_index) 191 + .ok_or(IntifaceRestError::InvalidFeature(index, feature_index))? 192 + .feature() 193 + .clone() 194 + .into(), 195 + ) 60 196 } 61 197 62 198 impl IntifaceRestServer { ··· 72 208 .route("/start-scanning", get(start_scanning)) 73 209 .route("/stop-scanning", get(stop_scanning)) 74 210 .route("/devices", get(get_devices)) 75 - .route("/devices/{index}/vibrate/{level}", post(set_device_vibrate_speed)) 211 + .route("/devices/stop", put(stop_all_devices)) 212 + .route("/devices/{index}", put(get_device)) 213 + .route("/devices/{index}/stop", put(stop_device)) 214 + .route("/devices/{index}/features", get(get_features)) 215 + .route("/devices/{index}/features/{index}/", put(get_feature)) 216 + .route( 217 + "/devices/{index}/outputs/{output_type}/", 218 + put(set_device_output), 219 + ) 220 + /* 221 + .route( 222 + "/devices/{index}/features/{index}/outputs/{output_type}/", 223 + put(set_feature_output), 224 + ) 225 + .route( 226 + "/devices/{index}/inputs/{input_type}/{input_command}", 227 + put(set_device_input), 228 + ) 229 + .route( 230 + "/devices/{index}/features/{index}/inputs/{input_type}/{input_command}", 231 + put(set_feature_input), 232 + ) 233 + .route("/devices/{index}/events", get(device_sse)) 234 + .route("/events", get(server_sse)) 235 + */ 76 236 //.route("/devices/{*index}/vibrate", post(set_feature_vibrate_speed)) 77 237 .with_state(Arc::new(client)); 78 238