message.rs (23415B)
1 use crate::error::RadrootsNostrConnectError; 2 use crate::method::RadrootsNostrConnectMethod; 3 use crate::permission::RadrootsNostrConnectPermissions; 4 use nostr::{Event, JsonUtil, PublicKey, RelayUrl, UnsignedEvent}; 5 use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 use serde_json::{Value, json}; 7 use std::str::FromStr; 8 use url::Url; 9 10 pub const RADROOTS_NOSTR_CONNECT_RPC_KIND: u16 = 24_133; 11 12 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 13 pub struct RadrootsNostrConnectRemoteSessionCapability { 14 pub user_public_key: PublicKey, 15 pub relays: Vec<RelayUrl>, 16 pub permissions: RadrootsNostrConnectPermissions, 17 } 18 19 #[derive(Debug, Clone, PartialEq, Eq)] 20 pub enum RadrootsNostrConnectRequest { 21 Connect { 22 remote_signer_public_key: PublicKey, 23 secret: Option<String>, 24 requested_permissions: RadrootsNostrConnectPermissions, 25 }, 26 GetPublicKey, 27 GetSessionCapability, 28 SignEvent(UnsignedEvent), 29 Nip04Encrypt { 30 public_key: PublicKey, 31 plaintext: String, 32 }, 33 Nip04Decrypt { 34 public_key: PublicKey, 35 ciphertext: String, 36 }, 37 Nip44Encrypt { 38 public_key: PublicKey, 39 plaintext: String, 40 }, 41 Nip44Decrypt { 42 public_key: PublicKey, 43 ciphertext: String, 44 }, 45 Ping, 46 SwitchRelays, 47 Custom { 48 method: RadrootsNostrConnectMethod, 49 params: Vec<String>, 50 }, 51 } 52 53 impl RadrootsNostrConnectRequest { 54 pub fn method(&self) -> RadrootsNostrConnectMethod { 55 match self { 56 Self::Connect { .. } => RadrootsNostrConnectMethod::Connect, 57 Self::GetPublicKey => RadrootsNostrConnectMethod::GetPublicKey, 58 Self::GetSessionCapability => RadrootsNostrConnectMethod::GetSessionCapability, 59 Self::SignEvent(_) => RadrootsNostrConnectMethod::SignEvent, 60 Self::Nip04Encrypt { .. } => RadrootsNostrConnectMethod::Nip04Encrypt, 61 Self::Nip04Decrypt { .. } => RadrootsNostrConnectMethod::Nip04Decrypt, 62 Self::Nip44Encrypt { .. } => RadrootsNostrConnectMethod::Nip44Encrypt, 63 Self::Nip44Decrypt { .. } => RadrootsNostrConnectMethod::Nip44Decrypt, 64 Self::Ping => RadrootsNostrConnectMethod::Ping, 65 Self::SwitchRelays => RadrootsNostrConnectMethod::SwitchRelays, 66 Self::Custom { method, .. } => method.clone(), 67 } 68 } 69 70 pub fn to_params(&self) -> Vec<String> { 71 match self { 72 Self::Connect { 73 remote_signer_public_key, 74 secret, 75 requested_permissions, 76 } => { 77 let mut params = vec![remote_signer_public_key.to_hex()]; 78 let normalized_secret = secret.as_ref().filter(|value| !value.is_empty()).cloned(); 79 if normalized_secret.is_some() || !requested_permissions.is_empty() { 80 params.push(normalized_secret.unwrap_or_default()); 81 } 82 if !requested_permissions.is_empty() { 83 params.push(requested_permissions.to_string()); 84 } 85 params 86 } 87 Self::GetPublicKey | Self::GetSessionCapability | Self::Ping | Self::SwitchRelays => { 88 Vec::new() 89 } 90 Self::SignEvent(unsigned_event) => vec![unsigned_event.as_json()], 91 Self::Nip04Encrypt { 92 public_key, 93 plaintext, 94 } 95 | Self::Nip44Encrypt { 96 public_key, 97 plaintext, 98 } => vec![public_key.to_hex(), plaintext.clone()], 99 Self::Nip04Decrypt { 100 public_key, 101 ciphertext, 102 } 103 | Self::Nip44Decrypt { 104 public_key, 105 ciphertext, 106 } => vec![public_key.to_hex(), ciphertext.clone()], 107 Self::Custom { params, .. } => params.clone(), 108 } 109 } 110 111 pub fn from_parts( 112 method: RadrootsNostrConnectMethod, 113 params: Vec<String>, 114 ) -> Result<Self, RadrootsNostrConnectError> { 115 match method { 116 RadrootsNostrConnectMethod::Connect => { 117 if params.is_empty() || params.len() > 3 { 118 return Err(RadrootsNostrConnectError::InvalidParams { 119 method: method.to_string(), 120 expected: "1 to 3 params", 121 received: params.len(), 122 }); 123 } 124 let remote_signer_public_key = parse_public_key(¶ms[0])?; 125 let secret = params 126 .get(1) 127 .cloned() 128 .and_then(|value| if value.is_empty() { None } else { Some(value) }); 129 let requested_permissions = match params.get(2) { 130 Some(value) => RadrootsNostrConnectPermissions::from_str(value)?, 131 None => RadrootsNostrConnectPermissions::default(), 132 }; 133 Ok(Self::Connect { 134 remote_signer_public_key, 135 secret, 136 requested_permissions, 137 }) 138 } 139 RadrootsNostrConnectMethod::GetPublicKey => { 140 expect_param_count(&method, ¶ms, 0)?; 141 Ok(Self::GetPublicKey) 142 } 143 RadrootsNostrConnectMethod::GetSessionCapability => { 144 expect_param_count(&method, ¶ms, 0)?; 145 Ok(Self::GetSessionCapability) 146 } 147 RadrootsNostrConnectMethod::SignEvent => { 148 expect_param_count(&method, ¶ms, 1)?; 149 let unsigned_event = serde_json::from_str(¶ms[0]).map_err(|error| { 150 RadrootsNostrConnectError::InvalidRequestPayload { 151 method: method.to_string(), 152 reason: error.to_string(), 153 } 154 })?; 155 Ok(Self::SignEvent(unsigned_event)) 156 } 157 RadrootsNostrConnectMethod::Nip04Encrypt => { 158 expect_param_count(&method, ¶ms, 2)?; 159 Ok(Self::Nip04Encrypt { 160 public_key: parse_public_key(¶ms[0])?, 161 plaintext: params[1].clone(), 162 }) 163 } 164 RadrootsNostrConnectMethod::Nip04Decrypt => { 165 expect_param_count(&method, ¶ms, 2)?; 166 Ok(Self::Nip04Decrypt { 167 public_key: parse_public_key(¶ms[0])?, 168 ciphertext: params[1].clone(), 169 }) 170 } 171 RadrootsNostrConnectMethod::Nip44Encrypt => { 172 expect_param_count(&method, ¶ms, 2)?; 173 Ok(Self::Nip44Encrypt { 174 public_key: parse_public_key(¶ms[0])?, 175 plaintext: params[1].clone(), 176 }) 177 } 178 RadrootsNostrConnectMethod::Nip44Decrypt => { 179 expect_param_count(&method, ¶ms, 2)?; 180 Ok(Self::Nip44Decrypt { 181 public_key: parse_public_key(¶ms[0])?, 182 ciphertext: params[1].clone(), 183 }) 184 } 185 RadrootsNostrConnectMethod::Ping => { 186 expect_param_count(&method, ¶ms, 0)?; 187 Ok(Self::Ping) 188 } 189 RadrootsNostrConnectMethod::SwitchRelays => { 190 expect_param_count(&method, ¶ms, 0)?; 191 Ok(Self::SwitchRelays) 192 } 193 custom => Ok(Self::Custom { 194 method: custom, 195 params, 196 }), 197 } 198 } 199 } 200 201 #[derive(Debug, Clone, PartialEq, Eq)] 202 pub struct RadrootsNostrConnectRequestMessage { 203 pub id: String, 204 pub request: RadrootsNostrConnectRequest, 205 } 206 207 impl RadrootsNostrConnectRequestMessage { 208 pub fn new(id: impl Into<String>, request: RadrootsNostrConnectRequest) -> Self { 209 Self { 210 id: id.into(), 211 request, 212 } 213 } 214 215 fn into_raw(self) -> RawRequestMessage { 216 RawRequestMessage { 217 id: self.id, 218 method: self.request.method(), 219 params: self.request.to_params(), 220 } 221 } 222 223 fn from_raw(raw: RawRequestMessage) -> Result<Self, RadrootsNostrConnectError> { 224 Ok(Self { 225 id: raw.id, 226 request: RadrootsNostrConnectRequest::from_parts(raw.method, raw.params)?, 227 }) 228 } 229 } 230 231 impl Serialize for RadrootsNostrConnectRequestMessage { 232 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 233 where 234 S: Serializer, 235 { 236 self.clone().into_raw().serialize(serializer) 237 } 238 } 239 240 impl<'de> Deserialize<'de> for RadrootsNostrConnectRequestMessage { 241 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 242 where 243 D: Deserializer<'de>, 244 { 245 let raw = RawRequestMessage::deserialize(deserializer)?; 246 Self::from_raw(raw).map_err(serde::de::Error::custom) 247 } 248 } 249 250 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 251 pub struct RadrootsNostrConnectResponseEnvelope { 252 pub id: String, 253 #[serde(default, skip_serializing_if = "Option::is_none")] 254 pub result: Option<Value>, 255 #[serde(default, skip_serializing_if = "Option::is_none")] 256 pub error: Option<String>, 257 } 258 259 pub const RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR: &str = "connection is pending"; 260 261 #[derive(Debug, Clone, PartialEq, Eq)] 262 pub enum RadrootsNostrConnectPendingConnectionPollOutcome { 263 PendingApproval, 264 Approved(PublicKey), 265 ApprovedCapability(RadrootsNostrConnectRemoteSessionCapability), 266 Rejected { message: String }, 267 AuthChallenge { url: String }, 268 UnexpectedResponse { response: String }, 269 } 270 271 #[derive(Debug, Clone, PartialEq, Eq)] 272 pub enum RadrootsNostrConnectResponse { 273 ConnectAcknowledged, 274 ConnectSecretEcho(String), 275 PendingConnection, 276 UserPublicKey(PublicKey), 277 RemoteSessionCapability(RadrootsNostrConnectRemoteSessionCapability), 278 SignedEvent(Event), 279 Pong, 280 Nip04Encrypt(String), 281 Nip04Decrypt(String), 282 Nip44Encrypt(String), 283 Nip44Decrypt(String), 284 RelayList(Vec<RelayUrl>), 285 RelayListUnchanged, 286 AuthUrl(String), 287 Error { 288 result: Option<Value>, 289 error: String, 290 }, 291 Custom { 292 result: Option<Value>, 293 error: Option<String>, 294 }, 295 } 296 297 impl RadrootsNostrConnectResponse { 298 pub fn into_pending_connection_poll_outcome( 299 self, 300 ) -> RadrootsNostrConnectPendingConnectionPollOutcome { 301 match self { 302 Self::PendingConnection => { 303 RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval 304 } 305 Self::UserPublicKey(public_key) => { 306 RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key) 307 } 308 Self::RemoteSessionCapability(capability) => { 309 RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) 310 } 311 Self::Error { error, .. } 312 if error == RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR => 313 { 314 RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval 315 } 316 Self::Error { error, .. } => { 317 RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message: error } 318 } 319 Self::AuthUrl(url) => { 320 RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge { url } 321 } 322 other => RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse { 323 response: format!("{other:?}"), 324 }, 325 } 326 } 327 328 pub fn into_envelope( 329 self, 330 id: impl Into<String>, 331 ) -> Result<RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectError> { 332 let id = id.into(); 333 let envelope = match self { 334 Self::ConnectAcknowledged => RadrootsNostrConnectResponseEnvelope { 335 id, 336 result: Some(Value::String("ack".to_owned())), 337 error: None, 338 }, 339 Self::ConnectSecretEcho(secret) => RadrootsNostrConnectResponseEnvelope { 340 id, 341 result: Some(Value::String(secret)), 342 error: None, 343 }, 344 Self::PendingConnection => RadrootsNostrConnectResponseEnvelope { 345 id, 346 result: None, 347 error: Some(RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned()), 348 }, 349 Self::UserPublicKey(public_key) => RadrootsNostrConnectResponseEnvelope { 350 id, 351 result: Some(Value::String(public_key.to_hex())), 352 error: None, 353 }, 354 Self::RemoteSessionCapability(capability) => RadrootsNostrConnectResponseEnvelope { 355 id, 356 result: Some(remote_session_capability_value(capability)), 357 error: None, 358 }, 359 Self::SignedEvent(event) => RadrootsNostrConnectResponseEnvelope { 360 id, 361 result: Some(Value::String(event.as_json())), 362 error: None, 363 }, 364 Self::Pong => RadrootsNostrConnectResponseEnvelope { 365 id, 366 result: Some(Value::String("pong".to_owned())), 367 error: None, 368 }, 369 Self::Nip04Encrypt(text) 370 | Self::Nip04Decrypt(text) 371 | Self::Nip44Encrypt(text) 372 | Self::Nip44Decrypt(text) => RadrootsNostrConnectResponseEnvelope { 373 id, 374 result: Some(Value::String(text)), 375 error: None, 376 }, 377 Self::RelayList(relays) => { 378 let relays = relays 379 .into_iter() 380 .map(|relay| relay.to_string()) 381 .collect::<Vec<_>>(); 382 RadrootsNostrConnectResponseEnvelope { 383 id, 384 result: Some(Value::Array( 385 relays.into_iter().map(Value::String).collect(), 386 )), 387 error: None, 388 } 389 } 390 Self::RelayListUnchanged => RadrootsNostrConnectResponseEnvelope { 391 id, 392 result: Some(Value::Null), 393 error: None, 394 }, 395 Self::AuthUrl(url) => { 396 let normalized = validate_url(&url)?; 397 RadrootsNostrConnectResponseEnvelope { 398 id, 399 result: Some(Value::String("auth_url".to_owned())), 400 error: Some(normalized), 401 } 402 } 403 Self::Error { result, error } => RadrootsNostrConnectResponseEnvelope { 404 id, 405 result, 406 error: Some(error), 407 }, 408 Self::Custom { result, error } => { 409 RadrootsNostrConnectResponseEnvelope { id, result, error } 410 } 411 }; 412 Ok(envelope) 413 } 414 415 pub fn from_envelope( 416 method: &RadrootsNostrConnectMethod, 417 envelope: RadrootsNostrConnectResponseEnvelope, 418 ) -> Result<Self, RadrootsNostrConnectError> { 419 if let (Some(Value::String(result)), Some(url)) = (&envelope.result, &envelope.error) 420 && result == "auth_url" 421 { 422 return Ok(Self::AuthUrl(validate_url(url)?)); 423 } 424 425 if let Some(error) = envelope.error { 426 if matches!( 427 method, 428 RadrootsNostrConnectMethod::GetPublicKey 429 | RadrootsNostrConnectMethod::GetSessionCapability 430 ) && envelope.result.is_none() 431 && error == RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR 432 { 433 return Ok(Self::PendingConnection); 434 } 435 if let RadrootsNostrConnectMethod::Custom(_) = method { 436 return Ok(Self::Custom { 437 result: envelope.result, 438 error: Some(error), 439 }); 440 } 441 return Ok(Self::Error { 442 result: envelope.result, 443 error, 444 }); 445 } 446 447 match method { 448 RadrootsNostrConnectMethod::Connect => { 449 let result = expect_string_result(method, envelope.result)?; 450 if result == "ack" { 451 Ok(Self::ConnectAcknowledged) 452 } else { 453 Ok(Self::ConnectSecretEcho(result)) 454 } 455 } 456 RadrootsNostrConnectMethod::GetPublicKey => { 457 let result = expect_string_result(method, envelope.result)?; 458 Ok(Self::UserPublicKey(parse_public_key(&result)?)) 459 } 460 RadrootsNostrConnectMethod::GetSessionCapability => { 461 let capability = parse_json_string_result(method, envelope.result)?; 462 Ok(Self::RemoteSessionCapability(capability)) 463 } 464 RadrootsNostrConnectMethod::SignEvent => { 465 let event = parse_json_string_result::<Event>(method, envelope.result)?; 466 Ok(Self::SignedEvent(event)) 467 } 468 RadrootsNostrConnectMethod::Ping => { 469 let result = expect_string_result(method, envelope.result)?; 470 if result != "pong" { 471 return Err(RadrootsNostrConnectError::InvalidResponsePayload { 472 method: method.to_string(), 473 reason: format!("expected `pong`, got `{result}`"), 474 }); 475 } 476 Ok(Self::Pong) 477 } 478 RadrootsNostrConnectMethod::Nip04Encrypt => Ok(Self::Nip04Encrypt( 479 expect_string_result(method, envelope.result)?, 480 )), 481 RadrootsNostrConnectMethod::Nip04Decrypt => Ok(Self::Nip04Decrypt( 482 expect_string_result(method, envelope.result)?, 483 )), 484 RadrootsNostrConnectMethod::Nip44Encrypt => Ok(Self::Nip44Encrypt( 485 expect_string_result(method, envelope.result)?, 486 )), 487 RadrootsNostrConnectMethod::Nip44Decrypt => Ok(Self::Nip44Decrypt( 488 expect_string_result(method, envelope.result)?, 489 )), 490 RadrootsNostrConnectMethod::SwitchRelays => { 491 parse_switch_relays_response(envelope.result) 492 } 493 RadrootsNostrConnectMethod::Custom(_) => Ok(Self::Custom { 494 result: envelope.result, 495 error: None, 496 }), 497 } 498 } 499 } 500 501 fn remote_session_capability_value( 502 capability: RadrootsNostrConnectRemoteSessionCapability, 503 ) -> Value { 504 json!({ 505 "user_public_key": capability.user_public_key, 506 "relays": capability.relays, 507 "permissions": capability.permissions, 508 }) 509 } 510 511 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 512 struct RawRequestMessage { 513 id: String, 514 method: RadrootsNostrConnectMethod, 515 params: Vec<String>, 516 } 517 518 fn expect_param_count( 519 method: &RadrootsNostrConnectMethod, 520 params: &[String], 521 expected: usize, 522 ) -> Result<(), RadrootsNostrConnectError> { 523 if params.len() == expected { 524 return Ok(()); 525 } 526 527 Err(RadrootsNostrConnectError::InvalidParams { 528 method: method.to_string(), 529 expected: if expected == 0 { 530 "no params" 531 } else if expected == 1 { 532 "exactly 1 param" 533 } else { 534 "exactly 2 params" 535 }, 536 received: params.len(), 537 }) 538 } 539 540 fn parse_public_key(value: &str) -> Result<PublicKey, RadrootsNostrConnectError> { 541 PublicKey::parse(value) 542 .or_else(|_| PublicKey::from_hex(value)) 543 .map_err(|error| RadrootsNostrConnectError::InvalidPublicKey { 544 value: value.to_owned(), 545 reason: error.to_string(), 546 }) 547 } 548 549 fn expect_string_result( 550 method: &RadrootsNostrConnectMethod, 551 result: Option<Value>, 552 ) -> Result<String, RadrootsNostrConnectError> { 553 match result { 554 Some(Value::String(value)) => Ok(value), 555 Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload { 556 method: method.to_string(), 557 reason: format!("expected string result, got {other}"), 558 }), 559 None => Err(RadrootsNostrConnectError::MissingResult), 560 } 561 } 562 563 fn parse_json_string_result<T>( 564 method: &RadrootsNostrConnectMethod, 565 result: Option<Value>, 566 ) -> Result<T, RadrootsNostrConnectError> 567 where 568 T: for<'de> Deserialize<'de>, 569 { 570 match result { 571 Some(Value::String(value)) => serde_json::from_str(&value).map_err(|error| { 572 RadrootsNostrConnectError::InvalidResponsePayload { 573 method: method.to_string(), 574 reason: error.to_string(), 575 } 576 }), 577 Some(other) => serde_json::from_value(other).map_err(|error| { 578 RadrootsNostrConnectError::InvalidResponsePayload { 579 method: method.to_string(), 580 reason: error.to_string(), 581 } 582 }), 583 None => Err(RadrootsNostrConnectError::MissingResult), 584 } 585 } 586 587 fn parse_switch_relays_response( 588 result: Option<Value>, 589 ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrConnectError> { 590 let method = RadrootsNostrConnectMethod::SwitchRelays; 591 match result { 592 None | Some(Value::Null) => Ok(RadrootsNostrConnectResponse::RelayListUnchanged), 593 Some(Value::Array(values)) => { 594 let relays = parse_relay_values(values)?; 595 Ok(RadrootsNostrConnectResponse::RelayList(relays)) 596 } 597 Some(Value::String(value)) if value == "null" => { 598 Ok(RadrootsNostrConnectResponse::RelayListUnchanged) 599 } 600 Some(Value::String(value)) => { 601 let parsed = serde_json::from_str::<Value>(&value).map_err(|error| { 602 RadrootsNostrConnectError::InvalidResponsePayload { 603 method: method.to_string(), 604 reason: error.to_string(), 605 } 606 })?; 607 parse_switch_relays_response(Some(parsed)) 608 } 609 Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload { 610 method: method.to_string(), 611 reason: format!("expected relay list or null, got {other}"), 612 }), 613 } 614 } 615 616 fn parse_relay_values(values: Vec<Value>) -> Result<Vec<RelayUrl>, RadrootsNostrConnectError> { 617 values 618 .into_iter() 619 .map(|value| match value { 620 Value::String(value) => RelayUrl::parse(&value).map_err(|error| { 621 RadrootsNostrConnectError::InvalidRelayUrl { 622 value, 623 reason: error.to_string(), 624 } 625 }), 626 other => Err(RadrootsNostrConnectError::InvalidResponsePayload { 627 method: RadrootsNostrConnectMethod::SwitchRelays.to_string(), 628 reason: format!("expected relay string, got {other}"), 629 }), 630 }) 631 .collect() 632 } 633 634 fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> { 635 Url::parse(value) 636 .map(|url| url.to_string()) 637 .map_err(|error| RadrootsNostrConnectError::InvalidUrl { 638 value: value.to_owned(), 639 reason: error.to_string(), 640 }) 641 }