use std::future::Future;
use std::pin::Pin;
use anyhow::{Context, Result};
use config::Config;
use map_core::client::MapClient;
use pbap_core::client::PbapClient;
use tokio_util::either::Either;
use transport::iroh::{Endpoint, EndpointId, HubStream, MAP_ALPN, PBAP_ALPN};
pub(crate) type Stream = Either<HubStream, bluer::rfcomm::Stream>;
pub(crate) enum Target {
Hub(EndpointId),
Rfcomm(bluer::Address, u8),
}
fn target(
hub: bool,
hub_node_key: Option<&str>,
device_override: Option<&str>,
device_addr: &str,
channel: u8,
) -> Result<Target> {
if hub {
Ok(Target::Hub(resolve_hub_id(hub_node_key)?))
} else {
let addr_str = device_override.unwrap_or(device_addr);
let addr = addr_str
.parse::<bluer::Address>()
.with_context(|| format!("invalid device address: {addr_str}"))?;
Ok(Target::Rfcomm(addr, channel))
}
}
pub(crate) fn resolve_hub_id(node_key: Option<&str>) -> Result<EndpointId> {
let key_str = node_key
.ok_or_else(|| anyhow::anyhow!("hub.node_key is not set; run `imsg spoke add <KEY>`"))?;
key_str.parse::<EndpointId>().context("invalid hub.node_key")
}
pub(crate) fn connect_map<'a>(
cfg: &'a Config,
endpoint: Option<&'a Endpoint>,
device_override: Option<&'a str>,
) -> Pin<Box<dyn Future<Output = Result<MapClient<Stream>>> + Send + 'a>> {
Box::pin(connect_map_inner(cfg, endpoint, device_override))
}
async fn connect_map_inner(
cfg: &Config,
endpoint: Option<&Endpoint>,
device_override: Option<&str>,
) -> Result<MapClient<Stream>> {
let tgt = target(
endpoint.is_some(),
cfg.hub.node_key.as_deref(),
device_override,
cfg.device.address(),
cfg.device.map_channel,
)?;
match tgt {
Target::Hub(id) => {
let ep = endpoint
.ok_or_else(|| anyhow::anyhow!("internal: hub target requires an endpoint"))?;
let conn = ep.connect(id, MAP_ALPN).await.context("iroh connect (MAP)")?;
let (send, recv) = conn.open_bi().await.context("iroh open_bi (MAP)")?;
let stream: Stream = Either::Left(HubStream::new(tokio::io::join(recv, send), conn));
session::lifecycle::establish_map_session(stream)
.await
.context("establishing MAP session over iroh")
}
Target::Rfcomm(addr, channel) => {
let stream =
transport::rfcomm::connect(addr, channel).await.context("RFCOMM connect (MAP)")?;
session::lifecycle::establish_map_session(Either::Right(stream))
.await
.context("establishing MAP session")
}
}
}
pub(crate) fn connect_pbap<'a>(
cfg: &'a Config,
endpoint: Option<&'a Endpoint>,
device_override: Option<&'a str>,
) -> Pin<Box<dyn Future<Output = Result<PbapClient<Stream>>> + Send + 'a>> {
Box::pin(connect_pbap_inner(cfg, endpoint, device_override))
}
async fn connect_pbap_inner(
cfg: &Config,
endpoint: Option<&Endpoint>,
device_override: Option<&str>,
) -> Result<PbapClient<Stream>> {
let tgt = target(
endpoint.is_some(),
cfg.hub.node_key.as_deref(),
device_override,
cfg.device.address(),
cfg.device.pbap_channel,
)?;
match tgt {
Target::Hub(id) => {
let ep = endpoint
.ok_or_else(|| anyhow::anyhow!("internal: hub target requires an endpoint"))?;
let conn = ep.connect(id, PBAP_ALPN).await.context("iroh connect (PBAP)")?;
let (send, recv) = conn.open_bi().await.context("iroh open_bi (PBAP)")?;
let stream: Stream = Either::Left(HubStream::new(tokio::io::join(recv, send), conn));
PbapClient::connect(stream).await.context("establishing PBAP session over iroh")
}
Target::Rfcomm(addr, channel) => {
let stream =
transport::rfcomm::connect(addr, channel).await.context("RFCOMM connect (PBAP)")?;
PbapClient::connect(Either::Right(stream)).await.context("establishing PBAP session")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn target_hub_key_absent_returns_err() {
let result = target(true, None, None, "AA:BB:CC:DD:EE:FF", 2);
assert!(result.is_err());
}
#[test]
fn target_hub_key_invalid_returns_err() {
let result = target(true, Some("not-a-valid-key"), None, "AA:BB:CC:DD:EE:FF", 2);
assert!(result.is_err());
}
#[test]
fn target_hub_valid_key() -> Result<()> {
let key = transport::iroh::SecretKey::generate();
let id_str = key.public().to_string();
let tgt = target(true, Some(&id_str), None, "AA:BB:CC:DD:EE:FF", 2)?;
assert!(matches!(tgt, Target::Hub(_)));
Ok(())
}
#[test]
fn target_rfcomm_valid_addr() -> Result<()> {
let tgt = target(false, None, None, "AA:BB:CC:DD:EE:FF", 2)?;
assert!(matches!(tgt, Target::Rfcomm(_, 2)));
Ok(())
}
#[test]
fn target_rfcomm_device_override() -> Result<()> {
let tgt = target(false, None, Some("11:22:33:44:55:66"), "AA:BB:CC:DD:EE:FF", 13)?;
let Target::Rfcomm(addr, ch) = tgt else {
anyhow::bail!("expected Rfcomm");
};
assert_eq!(addr.to_string(), "11:22:33:44:55:66");
assert_eq!(ch, 13);
Ok(())
}
#[test]
fn target_rfcomm_invalid_addr_returns_err() {
let result = target(false, None, None, "not-a-mac", 2);
assert!(result.is_err());
}
}