1use anyhow::{bail, Context};
12use seyd_signal_client::Identity;
13use std::path::Path;
14use url::Url;
15
16pub fn api_base(signal_url: &str) -> anyhow::Result<Url> {
19 let mut url =
20 Url::parse(signal_url).with_context(|| format!("bad signal_url: {signal_url}"))?;
21 let scheme = match url.scheme() {
22 "wss" | "https" => "https",
23 "ws" | "http" => "http",
24 other => bail!("signal_url has scheme {other:?}; expected ws, wss, http or https"),
25 };
26 url.set_scheme(scheme)
27 .ok()
28 .context("could not rewrite the URL scheme")?;
29 url.set_path("");
30 url.set_query(None);
31 url.set_fragment(None);
32 Ok(url)
33}
34
35pub async fn run(
36 robot_id: &str,
37 signal_url: &str,
38 credential_path: &Path,
39 token: &str,
40) -> anyhow::Result<()> {
41 let identity = Identity::load_or_create(credential_path)
44 .with_context(|| format!("could not open {}", credential_path.display()))?;
45 let public_key = identity.public_key_b64();
46
47 let url = api_base(signal_url)?.join("api/v1/enrol")?;
48 tracing::info!(robot_id, %url, "enrolling");
49
50 let res = reqwest::Client::new()
51 .post(url.clone())
52 .json(&serde_json::json!({
53 "token": token,
54 "robot_id": robot_id,
55 "public_key": public_key,
56 }))
57 .send()
58 .await
59 .with_context(|| format!("could not reach {url}"))?;
60
61 let status = res.status();
62 let body = res.text().await.unwrap_or_default();
63 if status.is_success() {
64 println!("Enrolled {robot_id}.");
65 println!("Key: {public_key}");
66 println!("Credential: {}", credential_path.display());
67 println!("\nStart the robot with: seydd --config <your seydd.toml>");
68 return Ok(());
69 }
70
71 let detail: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
74 let code = detail.get("error").and_then(|v| v.as_str()).unwrap_or("");
75 match (status.as_u16(), code) {
76 (410, _) => bail!(
77 "this enrolment token cannot be used: it is expired, already redeemed, \
78 or the robot id {robot_id:?} is taken. Issue a new one in the console."
79 ),
80 (400, _) => bail!(
81 "the server rejected the request: {}",
82 detail
83 .get("message")
84 .and_then(|v| v.as_str())
85 .unwrap_or(&body)
86 ),
87 (404, _) => bail!(
88 "{url} returned 404 — is signal_url pointing at a Seyd signal server \
89 new enough to have enrolment?"
90 ),
91 _ => bail!("enrolment failed ({status}): {body}"),
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn derives_the_api_origin_from_the_signal_url() {
101 assert_eq!(
102 api_base("wss://signal.seyd.io/ws").unwrap().as_str(),
103 "https://signal.seyd.io/"
104 );
105 assert_eq!(
106 api_base("ws://localhost:8080/ws").unwrap().as_str(),
107 "http://localhost:8080/"
108 );
109 assert_eq!(
111 api_base("ws://host:1/ws?x=1#f").unwrap().as_str(),
112 "http://host:1/"
113 );
114 assert!(api_base("ftp://nope/ws").is_err());
115 }
116
117 #[test]
118 fn joins_the_enrol_path_onto_the_origin() {
119 let url = api_base("wss://signal.seyd.io/ws")
120 .unwrap()
121 .join("api/v1/enrol")
122 .unwrap();
123 assert_eq!(url.as_str(), "https://signal.seyd.io/api/v1/enrol");
124 }
125}