1use std::future::Future;
4use std::pin::Pin;
5
6use anyhow::{Context, Result};
7use config::Config;
8use map_core::client::MapClient;
9use pbap_core::client::PbapClient;
10use tokio_util::either::Either;
11use transport::iroh::{Endpoint, EndpointId, HubStream, MAP_ALPN, PBAP_ALPN};
12
13pub type Stream = Either<HubStream, bluer::rfcomm::Stream>;
15
16enum Target {
18 Hub(EndpointId),
20 Rfcomm(bluer::Address, u8),
22}
23
24fn target(
36 hub: bool,
37 hub_node_key: Option<&str>,
38 device_override: Option<&str>,
39 device_addr: &str,
40 channel: u8,
41) -> Result<Target> {
42 if hub {
43 Ok(Target::Hub(resolve_hub_id(hub_node_key)?))
44 } else {
45 let addr_str = device_override.unwrap_or(device_addr);
46 let addr = addr_str
47 .parse::<bluer::Address>()
48 .with_context(|| format!("invalid device address: {addr_str}"))?;
49 Ok(Target::Rfcomm(addr, channel))
50 }
51}
52
53pub fn resolve_hub_id(node_key: Option<&str>) -> Result<EndpointId> {
62 let key_str = node_key
63 .ok_or_else(|| anyhow::anyhow!("hub.node_key is not set; run `imsg spoke add <KEY>`"))?;
64 key_str.parse::<EndpointId>().context("invalid hub.node_key")
65}
66
67#[must_use]
79pub fn connect_map<'a>(
80 cfg: &'a Config,
81 endpoint: Option<&'a Endpoint>,
82 device_override: Option<&'a str>,
83) -> Pin<Box<dyn Future<Output = Result<MapClient<Stream>>> + Send + 'a>> {
84 Box::pin(connect_map_inner(cfg, endpoint, device_override))
86}
87
88async fn connect_map_inner(
89 cfg: &Config,
90 endpoint: Option<&Endpoint>,
91 device_override: Option<&str>,
92) -> Result<MapClient<Stream>> {
93 let tgt = target(
94 endpoint.is_some(),
95 cfg.hub.node_key.as_deref(),
96 device_override,
97 cfg.device.address(),
98 cfg.device.map_channel,
99 )?;
100 match tgt {
101 Target::Hub(id) => {
102 let ep = endpoint
103 .ok_or_else(|| anyhow::anyhow!("internal: hub target requires an endpoint"))?;
104 let conn = ep.connect(id, MAP_ALPN).await.context("iroh connect (MAP)")?;
105 let (send, recv) = conn.open_bi().await.context("iroh open_bi (MAP)")?;
106 let stream: Stream = Either::Left(HubStream::new(tokio::io::join(recv, send), conn));
107 crate::lifecycle::establish_map_session(stream)
108 .await
109 .context("establishing MAP session over iroh")
110 }
111 Target::Rfcomm(addr, channel) => {
112 let stream = transport::rfcomm::connect(
113 addr,
114 channel,
115 transport::rfcomm::DEFAULT_BT_CONNECTED_GATE,
116 )
117 .await
118 .context("RFCOMM connect (MAP)")?;
119 crate::lifecycle::establish_map_session(Either::Right(stream))
120 .await
121 .context("establishing MAP session")
122 }
123 }
124}
125
126#[must_use]
136pub fn connect_pbap<'a>(
137 cfg: &'a Config,
138 endpoint: Option<&'a Endpoint>,
139 device_override: Option<&'a str>,
140) -> Pin<Box<dyn Future<Output = Result<PbapClient<Stream>>> + Send + 'a>> {
141 Box::pin(connect_pbap_inner(cfg, endpoint, device_override))
143}
144
145async fn connect_pbap_inner(
146 cfg: &Config,
147 endpoint: Option<&Endpoint>,
148 device_override: Option<&str>,
149) -> Result<PbapClient<Stream>> {
150 let tgt = target(
151 endpoint.is_some(),
152 cfg.hub.node_key.as_deref(),
153 device_override,
154 cfg.device.address(),
155 cfg.device.pbap_channel,
156 )?;
157 match tgt {
158 Target::Hub(id) => {
159 let ep = endpoint
160 .ok_or_else(|| anyhow::anyhow!("internal: hub target requires an endpoint"))?;
161 let conn = ep.connect(id, PBAP_ALPN).await.context("iroh connect (PBAP)")?;
162 let (send, recv) = conn.open_bi().await.context("iroh open_bi (PBAP)")?;
163 let stream: Stream = Either::Left(HubStream::new(tokio::io::join(recv, send), conn));
164 PbapClient::connect(stream).await.context("establishing PBAP session over iroh")
165 }
166 Target::Rfcomm(addr, channel) => {
167 let stream = transport::rfcomm::connect(
168 addr,
169 channel,
170 transport::rfcomm::DEFAULT_BT_CONNECTED_GATE,
171 )
172 .await
173 .context("RFCOMM connect (PBAP)")?;
174 PbapClient::connect(Either::Right(stream)).await.context("establishing PBAP session")
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn target_hub_key_absent_returns_err() {
185 let result = target(true, None, None, "AA:BB:CC:DD:EE:FF", 2);
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn target_hub_key_invalid_returns_err() {
191 let result = target(true, Some("not-a-valid-key"), None, "AA:BB:CC:DD:EE:FF", 2);
192 assert!(result.is_err());
193 }
194
195 #[test]
196 fn target_hub_valid_key() -> Result<()> {
197 let key = transport::iroh::SecretKey::generate();
198 let id_str = key.public().to_string();
199 let tgt = target(true, Some(&id_str), None, "AA:BB:CC:DD:EE:FF", 2)?;
200 assert!(matches!(tgt, Target::Hub(_)));
201 Ok(())
202 }
203
204 #[test]
205 fn target_rfcomm_valid_addr() -> Result<()> {
206 let tgt = target(false, None, None, "AA:BB:CC:DD:EE:FF", 2)?;
207 assert!(matches!(tgt, Target::Rfcomm(_, 2)));
208 Ok(())
209 }
210
211 #[test]
212 fn target_rfcomm_device_override() -> Result<()> {
213 let tgt = target(false, None, Some("11:22:33:44:55:66"), "AA:BB:CC:DD:EE:FF", 13)?;
214 let Target::Rfcomm(addr, ch) = tgt else {
215 anyhow::bail!("expected Rfcomm");
216 };
217 assert_eq!(addr.to_string(), "11:22:33:44:55:66");
218 assert_eq!(ch, 13);
219 Ok(())
220 }
221
222 #[test]
223 fn target_rfcomm_invalid_addr_returns_err() {
224 let result = target(false, None, None, "not-a-mac", 2);
225 assert!(result.is_err());
226 }
227}