use simploxide_api_types::{
JsonObject,
client_api::{BadResponseError, ClientApi, ExtractResponse as _},
events::{Event, EventKind},
};
use tokio::sync::oneshot;
use std::sync::Mutex;
use crate::{Hook, util::TypeField};
type Slot = Mutex<Option<oneshot::Sender<Result<String, JsonObject>>>>;
pub struct CtrlHandle {
slot: Slot,
}
impl CtrlHandle {
pub(crate) fn new() -> Self {
Self {
slot: Mutex::new(None),
}
}
pub async fn accept_remote_ctrl<C: ClientApi>(
&self,
client: &C,
link: &str,
) -> Result<(), CtrlError<C::Error>> {
let (tx, rx) = oneshot::channel();
*self.slot.lock().unwrap() = Some(tx);
let res = try_accept(client, link, rx).await;
if res.is_err() {
self.slot.lock().unwrap().take();
}
res
}
fn emit(&self, value: Result<String, JsonObject>) {
if let Ok(mut slot) = self.slot.lock()
&& let Some(sender) = slot.take()
{
let _ = sender.send(value);
}
}
}
impl Hook for CtrlHandle {
fn should_intercept(&self, kind: EventKind) -> bool {
kind == EventKind::Undocumented
}
fn intercept_event(&self, event: Event) {
let Event::Undocumented(ref obj) = event else {
return;
};
match obj.get("type").and_then(|v| v.as_str()) {
Some("remoteCtrlSessionCode") => {
match obj.get("sessionCode").and_then(|v| v.as_str()) {
Some(code) => self.emit(Ok(code.to_owned())),
None => self.emit(Err(obj.clone())),
}
}
Some("remoteCtrlStopped") => {
self.emit(Err(obj.clone()));
}
_ => (),
}
}
}
async fn try_accept<C: ClientApi>(
client: &C,
link: &str,
rx: oneshot::Receiver<Result<String, JsonObject>>,
) -> Result<(), CtrlError<C::Error>> {
let raw = client
.send_raw(format!("/crc {link}"))
.await
.map_err(CtrlError::Api)?;
check_tag::<C>(&raw, "remoteCtrlConnecting")?;
let code = rx
.await
.map_err(|_| CtrlError::EventStreamClosed)?
.map_err(|obj| CtrlError::BadResponse(BadResponseError::Undocumented(obj)))?;
let raw = client
.send_raw(format!("/verify remote ctrl {code}"))
.await
.map_err(CtrlError::Api)?;
check_tag::<C>(&raw, "remoteCtrlConnected")?;
Ok(())
}
fn check_tag<C: ClientApi>(raw: &str, expected: &str) -> Result<(), CtrlError<C::Error>> {
let shape: C::ResponseShape<'_, TypeField<'_>> = serde_json::from_str(raw)
.map_err(|e| CtrlError::BadResponse(BadResponseError::InvalidJson(e)))?;
let tag = shape.extract_response().map_err(CtrlError::BadResponse)?;
if tag.typ != expected {
return Err(CtrlError::BadResponse(BadResponseError::Undocumented(
serde_json::json!({"expected": expected, "got": tag.typ}),
)));
}
Ok(())
}
#[derive(Debug)]
pub enum CtrlError<E> {
Api(E),
EventStreamClosed,
BadResponse(BadResponseError),
}
impl<E: std::fmt::Display> std::fmt::Display for CtrlError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Api(e) => write!(f, "{e}"),
Self::EventStreamClosed => {
write!(f, "event stream closed before session code arrived")
}
Self::BadResponse(e) => write!(f, "unexpected remote control response: {e}"),
}
}
}
impl<E: 'static + std::error::Error> std::error::Error for CtrlError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Api(e) => Some(e),
Self::EventStreamClosed => None,
Self::BadResponse(e) => Some(e),
}
}
}