agentmux_srv/server/app_api/
viewer.rs1use super::*;
11
12pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
13 register_pair_start(engine, state);
14 register_devices(engine, state);
15 register_revoke(engine, state);
16 register_agent_hidden(engine, state);
17}
18
19pub(crate) const NOT_LISTENING: &str =
21 "The viewer isn't listening on this network. Turn on LAN discovery to pair a device.";
22
23pub(crate) fn pair_start_impl(state: &AppState) -> Result<ViewerPairStartResult, String> {
25 let (Some(port), Some(host)) = (state.viewer.advertised_port(), state.viewer.pair_host()) else {
26 return Err(NOT_LISTENING.to_string());
27 };
28 let fingerprint = state.viewer.tls()?.fingerprint.clone();
29 let (code, ttl) = state.viewer.pairing.start();
30 let query = url::form_urlencoded::Serializer::new(String::new())
31 .append_pair("v", "1")
32 .append_pair("host", &host.to_string())
33 .append_pair("port", &port.to_string())
34 .append_pair("fp", &fingerprint)
35 .append_pair("code", &code)
36 .append_pair("hostname", &state.hostname)
37 .append_pair("channel", &crate::backend::reactive::registry::local_channel_id())
38 .finish();
39 Ok(ViewerPairStartResult {
40 url: format!("agentmux://pair?{query}"),
41 expires_ms: agentmux_common::time::now_ms() + ttl.as_millis() as i64,
42 })
43}
44
45fn register_pair_start(engine: &Arc<WshRpcEngine>, state: &AppState) {
46 let state = state.clone();
47 engine.register_typed(COMMAND_VIEWER_PAIR_START, move |_req: Option<NoArgsReq>, _ctx| {
48 let state = state.clone();
49 async move { pair_start_impl(&state) }
50 });
51}
52
53fn register_devices(engine: &Arc<WshRpcEngine>, state: &AppState) {
54 let mstore = state.mstore.clone();
55 engine.register_typed(COMMAND_VIEWER_DEVICES, move |_req: Option<NoArgsReq>, _ctx| {
56 let mstore = mstore.clone();
57 async move {
58 let devices = mstore
59 .viewer_device_list()
60 .map_err(|e| format!("viewer.devices: {e}"))?
61 .into_iter()
62 .map(|d| ViewerDeviceInfo {
63 device_id: d.device_id,
64 device_name: d.device_name,
65 created_ms: d.created_ms,
66 last_seen_ms: d.last_seen_ms,
67 })
68 .collect();
69 Ok(ViewerDevicesResult { devices })
70 }
71 });
72}
73
74pub(crate) fn revoke_impl(state: &AppState, device_id: &str) -> Result<ViewerRevokeResult, String> {
77 let revoked = state.mstore.viewer_device_delete(device_id).map_err(|e| format!("viewer.revoke: {e}"))?;
78 let feeds_closed = state.viewer.close_device_feeds(device_id);
79 state.viewer.forget_device(device_id);
80 if revoked {
81 tracing::info!(%device_id, feeds_closed, "viewer: device revoked");
82 }
83 Ok(ViewerRevokeResult { revoked, feeds_closed })
84}
85
86fn register_revoke(engine: &Arc<WshRpcEngine>, state: &AppState) {
87 let state = state.clone();
88 engine.register_typed(COMMAND_VIEWER_REVOKE, move |cmd: CommandViewerRevokeData, _ctx| {
89 let state = state.clone();
90 async move { revoke_impl(&state, &cmd.device_id) }
91 });
92}
93
94fn register_agent_hidden(engine: &Arc<WshRpcEngine>, state: &AppState) {
95 let mstore = state.mstore.clone();
96 engine.register_typed(COMMAND_VIEWER_AGENT_HIDDEN, move |cmd: CommandViewerAgentHiddenData, _ctx| {
97 let mstore = mstore.clone();
98 async move {
99 if let Some(hidden) = cmd.hidden {
100 let found = mstore
101 .agent_hide_from_devices_set(&cmd.agent_id, hidden)
102 .map_err(|e| format!("viewer.agent-hidden: {e}"))?;
103 if !found {
104 return Err(format!("viewer.agent-hidden: no agent {}", cmd.agent_id));
105 }
106 }
107 let hidden = mstore
108 .agent_hide_from_devices_get(&cmd.agent_id)
109 .map_err(|e| format!("viewer.agent-hidden: {e}"))?;
110 Ok(ViewerAgentHiddenResult { hidden })
111 }
112 });
113}