Buttplug sex toy control library
1use std::{
2 collections::BTreeMap, convert::Infallible, io, net::SocketAddr, sync::Arc,
3};
4
5use axum::{
6 Json, Router,
7 extract::{Path, State, rejection::JsonRejection},
8 http::StatusCode,
9 response::{
10 IntoResponse, Response, Sse,
11 sse::{Event, KeepAlive},
12 },
13 routing::{get, put},
14};
15use buttplug_client::{
16 ButtplugClient, ButtplugClientDevice, ButtplugClientError,
17 device::{ClientDeviceFeature, ClientDeviceOutputCommand},
18};
19use buttplug_client_in_process::ButtplugInProcessClientConnectorBuilder;
20use buttplug_core::message::{DeviceFeature, OutputType};
21use buttplug_server::ButtplugServer;
22use futures::{Stream, StreamExt};
23use serde::Serialize;
24use thiserror::Error;
25use tokio::net::TcpListener;
26
27#[derive(Error, Debug)]
28enum IntifaceRestError {
29 #[error("JsonRejection: {0}")]
30 JsonRejection(JsonRejection),
31 #[error("Library Error: {0}")]
32 ButtplugClientError(ButtplugClientError),
33 #[error("Device index {0} does not refer to a currently connected device.")]
34 InvalidDevice(u32),
35 #[error("Device index {0} feature index {1} does not refer to a valid device feature.")]
36 InvalidFeature(u32, u32),
37 /*
38 #[error("{0} is not a valid output type. Valid output types are: {1:?}")]
39 InvalidOutputType(String, Vec<OutputType>),
40 #[error("{0} is not a valid input type. Valid input types are: {1:?}")]
41 InvalidInputType(String, Vec<String>),
42 #[error("{0} is not a valid input commands. Valid input commands are: {1:?})")]
43 InvalidInputCommand(u32, Vec<String>),
44 #[error("Value {0} is not valid for the current command.)")]
45 InvalidValue(u32),
46 */
47}
48
49// Tell axum how `AppError` should be converted into a response.
50//
51// This is also a convenient place to log errors.
52impl IntoResponse for IntifaceRestError {
53 fn into_response(self) -> Response {
54 let (status, message) = match self {
55 IntifaceRestError::JsonRejection(rejection) => {
56 // This error is caused by bad user input so don't log it
57 (rejection.status(), rejection.body_text())
58 }
59 _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
60 };
61 (status, message).into_response()
62 }
63}
64
65impl From<JsonRejection> for IntifaceRestError {
66 fn from(rejection: JsonRejection) -> Self {
67 Self::JsonRejection(rejection)
68 }
69}
70
71impl From<ButtplugClientError> for IntifaceRestError {
72 fn from(error: ButtplugClientError) -> Self {
73 Self::ButtplugClientError(error)
74 }
75}
76
77#[derive(Serialize)]
78struct IntifaceRestDevice {
79 index: u32,
80 name: String,
81 display_name: Option<String>,
82 features: BTreeMap<u32, DeviceFeature>,
83}
84
85impl From<&ButtplugClientDevice> for IntifaceRestDevice {
86 fn from(device: &ButtplugClientDevice) -> Self {
87 Self {
88 index: device.index(),
89 name: device.name().clone(),
90 display_name: device.display_name().clone(),
91 features: device
92 .device_features()
93 .iter()
94 .map(|(i, d)| (*i, d.feature().clone()))
95 .collect(),
96 }
97 }
98}
99
100pub struct IntifaceRestServer {}
101
102fn get_device(
103 client: &ButtplugClient,
104 index: u32,
105) -> Result<ButtplugClientDevice, IntifaceRestError> {
106 client
107 .devices()
108 .get(&index)
109 .ok_or(IntifaceRestError::InvalidDevice(index))
110 .cloned()
111}
112
113fn get_feature(
114 client: &ButtplugClient,
115 index: u32,
116 feature_index: u32,
117) -> Result<ClientDeviceFeature, IntifaceRestError> {
118 get_device(client, index)?
119 .device_features()
120 .get(&feature_index)
121 .ok_or(IntifaceRestError::InvalidFeature(index, feature_index))
122 .cloned()
123}
124
125async fn start_scanning(
126 State(client): State<Arc<ButtplugClient>>,
127) -> Result<(), IntifaceRestError> {
128 client
129 .start_scanning()
130 .await
131 .map_err(IntifaceRestError::ButtplugClientError)
132}
133
134async fn stop_scanning(State(client): State<Arc<ButtplugClient>>) -> Result<(), IntifaceRestError> {
135 client
136 .stop_scanning()
137 .await
138 .map_err(IntifaceRestError::ButtplugClientError)
139}
140
141async fn stop_all_devices(
142 State(client): State<Arc<ButtplugClient>>,
143) -> Result<(), IntifaceRestError> {
144 client
145 .stop_all_devices()
146 .await
147 .map_err(IntifaceRestError::ButtplugClientError)
148}
149
150async fn stop_device(
151 State(client): State<Arc<ButtplugClient>>,
152 Path(index): Path<u32>,
153) -> Result<(), IntifaceRestError> {
154 get_device(&client, index)?
155 .stop()
156 .await
157 .map_err(IntifaceRestError::ButtplugClientError)
158}
159
160async fn set_device_output(
161 State(client): State<Arc<ButtplugClient>>,
162 Path((index, output_type, level)): Path<(u32, OutputType, f64)>,
163) -> Result<(), IntifaceRestError> {
164 let cmd = ClientDeviceOutputCommand::from_command_value_float(output_type, level)
165 .map_err(IntifaceRestError::ButtplugClientError)?;
166
167 get_device(&client, index)?
168 .send_command(&cmd)
169 .await
170 .map_err(IntifaceRestError::ButtplugClientError)
171}
172
173async fn set_feature_output(
174 State(client): State<Arc<ButtplugClient>>,
175 Path((index, feature_index, output_type, level)): Path<(u32, u32, OutputType, f64)>,
176) -> Result<(), IntifaceRestError> {
177 let cmd = ClientDeviceOutputCommand::from_command_value_float(output_type, level)
178 .map_err(IntifaceRestError::ButtplugClientError)?;
179
180 get_feature(&client, index, feature_index)?
181 .send_command(&cmd)
182 .await
183 .map_err(IntifaceRestError::ButtplugClientError)
184}
185
186async fn get_devices(
187 State(client): State<Arc<ButtplugClient>>,
188) -> Json<BTreeMap<u32, IntifaceRestDevice>> {
189 client
190 .devices()
191 .iter()
192 .map(|(i, x)| (*i, x.into()))
193 .collect::<BTreeMap<u32, IntifaceRestDevice>>()
194 .into()
195}
196
197async fn get_device_info(
198 State(client): State<Arc<ButtplugClient>>,
199 Path(index): Path<u32>,
200) -> Result<Json<IntifaceRestDevice>, IntifaceRestError> {
201 Ok(
202 IntifaceRestDevice::from(
203 client
204 .devices()
205 .get(&index)
206 .ok_or(IntifaceRestError::InvalidDevice(index))?,
207 )
208 .into(),
209 )
210}
211
212async fn get_features(
213 State(client): State<Arc<ButtplugClient>>,
214 Path(index): Path<u32>,
215) -> Result<Json<BTreeMap<u32, DeviceFeature>>, IntifaceRestError> {
216 Ok(
217 get_device(&client, index)?
218 .device_features()
219 .iter()
220 .map(|(i, f)| (*i, f.feature().clone()))
221 .collect::<BTreeMap<u32, DeviceFeature>>()
222 .into(),
223 )
224}
225
226async fn get_feature_info(
227 State(client): State<Arc<ButtplugClient>>,
228 Path((index, feature_index)): Path<(u32, u32)>,
229) -> Result<Json<DeviceFeature>, IntifaceRestError> {
230 Ok(
231 get_device(&client, index)?
232 .device_features()
233 .get(&feature_index)
234 .ok_or(IntifaceRestError::InvalidFeature(index, feature_index))?
235 .feature()
236 .clone()
237 .into(),
238 )
239}
240
241/*
242async fn feature_input_command(
243 State(client): State<Arc<ButtplugClient>>,
244 Path((index, feature_index, input_type, command)): Path<(u32, u32, String, String)>,
245) -> Result<(), IntifaceRestError> {
246 let cmd = convert_output_command(&command, level)?;
247
248 Ok(
249 get_feature(&client, index, feature_index)?
250 .send_command(&cmd)
251 .await
252 .map_err(|e| IntifaceRestError::ButtplugClientError(e))?,
253 )
254
255 Ok(())
256}
257*/
258
259async fn server_sse(
260 State(client): State<Arc<ButtplugClient>>,
261) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
262 let stream = client
263 .event_stream()
264 .map(|e| Ok(Event::default().data(format!("{:?}", e))));
265
266 Sse::new(stream).keep_alive(KeepAlive::default())
267}
268
269impl IntifaceRestServer {
270 pub async fn run(port: u16, server: ButtplugServer) -> Result<(), io::Error> {
271 let connector = ButtplugInProcessClientConnectorBuilder::default()
272 .server(server)
273 .finish();
274 let client = ButtplugClient::new("Intiface REST API");
275 client.connect(connector).await.unwrap();
276 info!("Setting up app!");
277 // pass incoming GET requests on "/hello-world" to "hello_world" handler.
278 let app = Router::new().nest(
279 "/api/v1",
280 Router::new()
281 .route("/start-scanning", get(start_scanning))
282 .route("/stop-scanning", get(stop_scanning))
283 .route("/devices", get(get_devices))
284 .route("/devices/stop", put(stop_all_devices))
285 .route("/devices/{index}", get(get_device_info))
286 .route("/devices/{index}/stop", put(stop_device))
287 .route("/devices/{index}/features", get(get_features))
288 .route("/devices/{index}/features/{index}/", put(get_feature_info))
289 .route(
290 "/devices/{index}/outputs/{output_type}/{level}",
291 put(set_device_output),
292 )
293 .route(
294 "/devices/{index}/features/{index}/outputs/{output_type}/{level}",
295 put(set_feature_output),
296 )
297 /*
298 .route(
299 "/devices/{index}/inputs/{input_type}/{input_command}",
300 put(device_input_command),
301 )
302 .route(
303 "/devices/{index}/features/{index}/inputs/{input_type}/{input_command}",
304 put(feature_input_command),
305 )
306 .route("/devices/{index}/events", get(device_sse))
307 */
308 .route("/events", get(server_sse))
309 //.route("/devices/{*index}/vibrate", post(set_feature_vibrate_speed))
310 .with_state(Arc::new(client)),
311 );
312
313 // write address like this to not make typos
314 let addr = SocketAddr::from(([127, 0, 0, 1], port));
315 info!("Buttplug REST API Server now listening on {:?}", addr);
316 let listener = TcpListener::bind(addr).await?;
317 axum::serve(listener, app.into_make_service()).await?;
318
319 Ok(())
320 }
321}