simploxide_client/
remote.rs1use simploxide_api_types::{
18 JsonObject,
19 client_api::{BadResponseError, ClientApi, ExtractResponse as _},
20 events::{Event, EventKind},
21};
22use tokio::sync::oneshot;
23
24use std::sync::Mutex;
25
26use crate::{Hook, util::TypeField};
27
28type Slot = Mutex<Option<oneshot::Sender<Result<String, JsonObject>>>>;
29
30pub struct CtrlHandle {
32 slot: Slot,
33}
34
35impl CtrlHandle {
36 pub(crate) fn new() -> Self {
37 Self {
38 slot: Mutex::new(None),
39 }
40 }
41
42 pub async fn accept_remote_ctrl<C: ClientApi>(
52 &self,
53 client: &C,
54 link: &str,
55 ) -> Result<(), CtrlError<C::Error>> {
56 let (tx, rx) = oneshot::channel();
57 *self.slot.lock().unwrap() = Some(tx);
58
59 let res = try_accept(client, link, rx).await;
60
61 if res.is_err() {
62 self.slot.lock().unwrap().take();
63 }
64
65 res
66 }
67
68 fn emit(&self, value: Result<String, JsonObject>) {
69 if let Ok(mut slot) = self.slot.lock()
70 && let Some(sender) = slot.take()
71 {
72 let _ = sender.send(value);
73 }
74 }
75}
76
77impl Hook for CtrlHandle {
78 fn should_intercept(&self, kind: EventKind) -> bool {
79 kind == EventKind::Undocumented
80 }
81
82 fn intercept_event(&self, event: Event) {
83 let Event::Undocumented(ref obj) = event else {
86 return;
87 };
88
89 match obj.get("type").and_then(|v| v.as_str()) {
90 Some("remoteCtrlSessionCode") => {
91 match obj.get("sessionCode").and_then(|v| v.as_str()) {
92 Some(code) => self.emit(Ok(code.to_owned())),
93 None => self.emit(Err(obj.clone())),
94 }
95 }
96 Some("remoteCtrlStopped") => {
97 self.emit(Err(obj.clone()));
98 }
99 _ => (),
100 }
101 }
102}
103
104async fn try_accept<C: ClientApi>(
105 client: &C,
106 link: &str,
107 rx: oneshot::Receiver<Result<String, JsonObject>>,
108) -> Result<(), CtrlError<C::Error>> {
109 let raw = client
110 .send_raw(format!("/crc {link}"))
111 .await
112 .map_err(CtrlError::Api)?;
113
114 check_tag::<C>(&raw, "remoteCtrlConnecting")?;
115
116 let code = rx
117 .await
118 .map_err(|_| CtrlError::EventStreamClosed)?
119 .map_err(|obj| CtrlError::BadResponse(BadResponseError::Undocumented(obj)))?;
120
121 let raw = client
122 .send_raw(format!("/verify remote ctrl {code}"))
123 .await
124 .map_err(CtrlError::Api)?;
125
126 check_tag::<C>(&raw, "remoteCtrlConnected")?;
127 Ok(())
128}
129
130fn check_tag<C: ClientApi>(raw: &str, expected: &str) -> Result<(), CtrlError<C::Error>> {
131 let shape: C::ResponseShape<'_, TypeField<'_>> = serde_json::from_str(raw)
132 .map_err(|e| CtrlError::BadResponse(BadResponseError::InvalidJson(e)))?;
133
134 let tag = shape.extract_response().map_err(CtrlError::BadResponse)?;
135
136 if tag.typ != expected {
137 return Err(CtrlError::BadResponse(BadResponseError::Undocumented(
138 serde_json::json!({"expected": expected, "got": tag.typ}),
139 )));
140 }
141 Ok(())
142}
143
144#[derive(Debug)]
145pub enum CtrlError<E> {
146 Api(E),
148 EventStreamClosed,
150 BadResponse(BadResponseError),
152}
153
154impl<E: std::fmt::Display> std::fmt::Display for CtrlError<E> {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 Self::Api(e) => write!(f, "{e}"),
158 Self::EventStreamClosed => {
159 write!(f, "event stream closed before session code arrived")
160 }
161 Self::BadResponse(e) => write!(f, "unexpected remote control response: {e}"),
162 }
163 }
164}
165
166impl<E: 'static + std::error::Error> std::error::Error for CtrlError<E> {
167 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168 match self {
169 Self::Api(e) => Some(e),
170 Self::EventStreamClosed => None,
171 Self::BadResponse(e) => Some(e),
172 }
173 }
174}