feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "buzz-ws-client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
nostr = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -0,0 +1,314 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use nostr::{Event, Keys, Tag};
|
||||
use serde_json::{json, Value};
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::error::WsClientError;
|
||||
use crate::message::{build_auth_event, parse_relay_message, OkResponse, RelayMessage};
|
||||
|
||||
type WsStream = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
|
||||
/// Seconds to wait for the relay to send the NIP-42 AUTH challenge after connecting.
|
||||
pub const AUTH_CHALLENGE_TIMEOUT_SECS: u64 = 20;
|
||||
|
||||
/// Seconds to wait for the relay's OK response to the AUTH event.
|
||||
pub const AUTH_OK_TIMEOUT_SECS: u64 = 20;
|
||||
|
||||
/// Seconds to wait for the relay's OK response to a published event.
|
||||
pub const PUBLISH_OK_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// A NIP-42-capable WebSocket connection to a Nostr relay.
|
||||
pub struct NostrWsConnection {
|
||||
ws: WsStream,
|
||||
buffer: VecDeque<RelayMessage>,
|
||||
pending_challenge: Option<String>,
|
||||
relay_url: String,
|
||||
}
|
||||
|
||||
impl NostrWsConnection {
|
||||
/// Connects to the relay at `url` and performs NIP-42 authentication with `keys`.
|
||||
///
|
||||
/// Pass `auth_tag` to include a NIP-OA authorization tag in the AUTH event.
|
||||
pub async fn connect_authenticated(
|
||||
url: &str,
|
||||
keys: &Keys,
|
||||
auth_tag: Option<&Tag>,
|
||||
) -> Result<Self, WsClientError> {
|
||||
let mut conn = Self::connect(url).await?;
|
||||
conn.authenticate(keys, auth_tag).await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Connects to the relay at `url` without performing authentication.
|
||||
pub async fn connect(url: &str) -> Result<Self, WsClientError> {
|
||||
let parsed = url
|
||||
.parse::<url::Url>()
|
||||
.map_err(|e| WsClientError::Url(e.to_string()))?;
|
||||
|
||||
let (ws, _response) = connect_async(parsed.as_str())
|
||||
.await
|
||||
.map_err(WsClientError::WebSocket)?;
|
||||
|
||||
debug!("connected to relay at {url}");
|
||||
|
||||
Ok(Self {
|
||||
ws,
|
||||
buffer: VecDeque::new(),
|
||||
pending_challenge: None,
|
||||
relay_url: url.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Performs NIP-42 authentication using `keys` against the connected relay.
|
||||
///
|
||||
/// Pass `auth_tag` to include a NIP-OA authorization tag in the AUTH event.
|
||||
pub async fn authenticate(
|
||||
&mut self,
|
||||
keys: &Keys,
|
||||
auth_tag: Option<&Tag>,
|
||||
) -> Result<(), WsClientError> {
|
||||
let challenge = self
|
||||
.wait_for_auth_challenge(Duration::from_secs(AUTH_CHALLENGE_TIMEOUT_SECS))
|
||||
.await?;
|
||||
|
||||
let auth_event = build_auth_event(&challenge, &self.relay_url, keys, auth_tag)?;
|
||||
let event_id = auth_event.id.to_hex();
|
||||
|
||||
self.send_raw(&json!(["AUTH", auth_event])).await?;
|
||||
|
||||
let ok = self
|
||||
.wait_for_ok(&event_id, Duration::from_secs(AUTH_OK_TIMEOUT_SECS))
|
||||
.await?;
|
||||
if !ok.accepted {
|
||||
return Err(WsClientError::AuthFailed(ok.message));
|
||||
}
|
||||
|
||||
debug!("NIP-42 authentication successful");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a signed event to the relay and waits for the OK response.
|
||||
pub async fn send_event(&mut self, event: Event) -> Result<OkResponse, WsClientError> {
|
||||
let event_id = event.id.to_hex();
|
||||
self.send_raw(&json!(["EVENT", event])).await?;
|
||||
self.wait_for_ok(&event_id, Duration::from_secs(PUBLISH_OK_TIMEOUT_SECS))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Receives the next relay message, waiting up to `timeout_dur`.
|
||||
pub async fn next_event(
|
||||
&mut self,
|
||||
timeout_dur: Duration,
|
||||
) -> Result<RelayMessage, WsClientError> {
|
||||
if let Some(msg) = self.buffer.pop_front() {
|
||||
return Ok(msg);
|
||||
}
|
||||
self.recv_one(timeout_dur).await
|
||||
}
|
||||
|
||||
/// Closes the WebSocket connection gracefully.
|
||||
pub async fn disconnect(mut self) -> Result<(), WsClientError> {
|
||||
self.ws.close(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a raw JSON value as a WebSocket text frame.
|
||||
pub async fn send_raw(&mut self, value: &Value) -> Result<(), WsClientError> {
|
||||
let text = serde_json::to_string(value)?;
|
||||
debug!("→ relay: {text}");
|
||||
self.ws.send(Message::Text(text.into())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_one(&mut self, timeout_dur: Duration) -> Result<RelayMessage, WsClientError> {
|
||||
if let Some(msg) = self.buffer.pop_front() {
|
||||
return Ok(msg);
|
||||
}
|
||||
|
||||
loop {
|
||||
let raw = timeout(timeout_dur, self.ws.next())
|
||||
.await
|
||||
.map_err(|_| WsClientError::Timeout)?
|
||||
.ok_or(WsClientError::ConnectionClosed)?
|
||||
.map_err(WsClientError::WebSocket)?;
|
||||
|
||||
match raw {
|
||||
Message::Text(text) => {
|
||||
let msg = parse_relay_message(&text)?;
|
||||
if let RelayMessage::Auth { ref challenge } = msg {
|
||||
self.pending_challenge = Some(challenge.clone());
|
||||
}
|
||||
return Ok(msg);
|
||||
}
|
||||
Message::Ping(data) => {
|
||||
self.ws.send(Message::Pong(data)).await?;
|
||||
}
|
||||
Message::Close(_) => return Err(WsClientError::ConnectionClosed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_auth_challenge(
|
||||
&mut self,
|
||||
timeout_dur: Duration,
|
||||
) -> Result<String, WsClientError> {
|
||||
if let Some(challenge) = self.pending_challenge.take() {
|
||||
return Ok(challenge);
|
||||
}
|
||||
|
||||
if let Some(idx) = self
|
||||
.buffer
|
||||
.iter()
|
||||
.position(|m| matches!(m, RelayMessage::Auth { .. }))
|
||||
{
|
||||
match self.buffer.remove(idx).unwrap() {
|
||||
RelayMessage::Auth { challenge } => return Ok(challenge),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
let deadline = tokio::time::Instant::now() + timeout_dur;
|
||||
|
||||
loop {
|
||||
let remaining = deadline
|
||||
.checked_duration_since(tokio::time::Instant::now())
|
||||
.unwrap_or(Duration::ZERO);
|
||||
|
||||
if remaining.is_zero() {
|
||||
return Err(WsClientError::NoAuthChallenge);
|
||||
}
|
||||
|
||||
let raw = timeout(remaining, self.ws.next())
|
||||
.await
|
||||
.map_err(|_| WsClientError::NoAuthChallenge)?
|
||||
.ok_or(WsClientError::ConnectionClosed)?
|
||||
.map_err(WsClientError::WebSocket)?;
|
||||
|
||||
match raw {
|
||||
Message::Text(text) => {
|
||||
let msg = parse_relay_message(&text)?;
|
||||
match msg {
|
||||
RelayMessage::Auth { challenge } => {
|
||||
if challenge.len() > 1024 {
|
||||
return Err(WsClientError::AuthFailed(
|
||||
"challenge exceeds 1024 bytes".into(),
|
||||
));
|
||||
}
|
||||
return Ok(challenge);
|
||||
}
|
||||
other => self.buffer.push_back(other),
|
||||
}
|
||||
}
|
||||
Message::Ping(data) => {
|
||||
self.ws.send(Message::Pong(data)).await?;
|
||||
}
|
||||
Message::Close(_) => return Err(WsClientError::ConnectionClosed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_ok(
|
||||
&mut self,
|
||||
event_id: &str,
|
||||
timeout_dur: Duration,
|
||||
) -> Result<OkResponse, WsClientError> {
|
||||
let deadline = tokio::time::Instant::now() + timeout_dur;
|
||||
|
||||
if let Some(idx) = self
|
||||
.buffer
|
||||
.iter()
|
||||
.position(|m| matches!(m, RelayMessage::Ok(ok) if ok.event_id == event_id))
|
||||
{
|
||||
match self.buffer.remove(idx).unwrap() {
|
||||
RelayMessage::Ok(ok) => return Ok(ok),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let remaining = deadline
|
||||
.checked_duration_since(tokio::time::Instant::now())
|
||||
.unwrap_or(Duration::ZERO);
|
||||
|
||||
if remaining.is_zero() {
|
||||
return Err(WsClientError::Timeout);
|
||||
}
|
||||
|
||||
let raw = timeout(remaining, self.ws.next())
|
||||
.await
|
||||
.map_err(|_| WsClientError::Timeout)?
|
||||
.ok_or(WsClientError::ConnectionClosed)?
|
||||
.map_err(WsClientError::WebSocket)?;
|
||||
|
||||
match raw {
|
||||
Message::Text(text) => {
|
||||
let msg = parse_relay_message(&text)?;
|
||||
match msg {
|
||||
RelayMessage::Ok(ok) if ok.event_id == event_id => return Ok(ok),
|
||||
RelayMessage::Auth { ref challenge } => {
|
||||
self.pending_challenge = Some(challenge.clone());
|
||||
self.buffer.push_back(msg);
|
||||
}
|
||||
other => self.buffer.push_back(other),
|
||||
}
|
||||
}
|
||||
Message::Ping(data) => {
|
||||
self.ws.send(Message::Pong(data)).await?;
|
||||
}
|
||||
Message::Close(_) => return Err(WsClientError::ConnectionClosed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One-shot helper: connect, authenticate, send one event, disconnect.
|
||||
///
|
||||
/// Establishes a fresh WebSocket connection, completes NIP-42 authentication,
|
||||
/// publishes `event`, waits for the relay's OK response, then closes the
|
||||
/// connection. The entire operation is bounded by `timeout_secs`.
|
||||
pub async fn publish_event(
|
||||
relay_url: &str,
|
||||
event: Event,
|
||||
keys: &Keys,
|
||||
auth_tag: Option<&Tag>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<OkResponse, WsClientError> {
|
||||
let result = tokio::time::timeout(Duration::from_secs(timeout_secs), async {
|
||||
let mut conn = NostrWsConnection::connect(relay_url).await?;
|
||||
conn.authenticate(keys, auth_tag).await?;
|
||||
let ok = conn.send_event(event).await?;
|
||||
let _ = conn.disconnect().await;
|
||||
Ok::<_, WsClientError>(ok)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| WsClientError::Timeout)?;
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn auth_challenge_timeout_meets_floor() {
|
||||
const { assert!(AUTH_CHALLENGE_TIMEOUT_SECS >= 20) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_ok_timeout_meets_floor() {
|
||||
const { assert!(AUTH_OK_TIMEOUT_SECS >= 20) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publish_ok_timeout_meets_floor() {
|
||||
const { assert!(PUBLISH_OK_TIMEOUT_SECS >= 30) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors returned by [`crate::NostrWsConnection`] and related operations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WsClientError {
|
||||
/// A WebSocket transport error occurred.
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
|
||||
|
||||
/// A JSON serialization or deserialization error occurred.
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
/// Failed to build a Nostr event.
|
||||
#[error("Nostr event builder error: {0}")]
|
||||
EventBuilder(String),
|
||||
|
||||
/// Failed to parse a URL.
|
||||
#[error("URL parse error: {0}")]
|
||||
Url(String),
|
||||
|
||||
/// The relay did not respond within the expected time.
|
||||
#[error("Timeout waiting for relay message")]
|
||||
Timeout,
|
||||
|
||||
/// The WebSocket connection was closed before the operation completed.
|
||||
#[error("Connection closed unexpectedly")]
|
||||
ConnectionClosed,
|
||||
|
||||
/// The relay sent a message that was not expected at this point.
|
||||
#[error("Unexpected relay message: {0}")]
|
||||
UnexpectedMessage(String),
|
||||
|
||||
/// NIP-42 authentication was rejected by the relay.
|
||||
#[error("Authentication failed: {0}")]
|
||||
AuthFailed(String),
|
||||
|
||||
/// The relay rejected the submitted event.
|
||||
#[error("Event rejected by relay: {0}")]
|
||||
EventRejected(String),
|
||||
|
||||
/// No NIP-42 AUTH challenge was received from the relay.
|
||||
#[error("No AUTH challenge received from relay")]
|
||||
NoAuthChallenge,
|
||||
}
|
||||
|
||||
impl From<nostr::event::builder::Error> for WsClientError {
|
||||
fn from(e: nostr::event::builder::Error) -> Self {
|
||||
WsClientError::EventBuilder(e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
pub mod connection;
|
||||
pub mod error;
|
||||
pub mod message;
|
||||
|
||||
pub use connection::{publish_event, NostrWsConnection};
|
||||
pub use error::WsClientError;
|
||||
pub use message::{build_auth_event, parse_relay_message, OkResponse, RelayMessage};
|
||||
@@ -0,0 +1,190 @@
|
||||
use nostr::{Event, EventBuilder, Keys, RelayUrl, Tag};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::WsClientError;
|
||||
|
||||
/// A message received from a Nostr relay.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RelayMessage {
|
||||
/// An event matching an active subscription.
|
||||
Event {
|
||||
/// The subscription ID this event belongs to.
|
||||
subscription_id: String,
|
||||
/// The Nostr event payload.
|
||||
event: Box<Event>,
|
||||
},
|
||||
/// Acknowledgement of a published event.
|
||||
Ok(OkResponse),
|
||||
/// End-of-stored-events marker for a subscription.
|
||||
Eose {
|
||||
/// The subscription ID that has reached end-of-stored-events.
|
||||
subscription_id: String,
|
||||
},
|
||||
/// The relay closed a subscription, usually with an error.
|
||||
Closed {
|
||||
/// The subscription ID that was closed.
|
||||
subscription_id: String,
|
||||
/// Human-readable reason for the closure.
|
||||
message: String,
|
||||
},
|
||||
/// A human-readable notice from the relay.
|
||||
Notice {
|
||||
/// The notice text.
|
||||
message: String,
|
||||
},
|
||||
/// A NIP-42 authentication challenge from the relay.
|
||||
Auth {
|
||||
/// The challenge string to sign.
|
||||
challenge: String,
|
||||
},
|
||||
/// A NIP-45 COUNT response.
|
||||
Count {
|
||||
/// The subscription ID this count belongs to.
|
||||
subscription_id: String,
|
||||
/// The number of matching events.
|
||||
count: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// The relay's response to a published event (NIP-01 `OK` message).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OkResponse {
|
||||
/// Hex-encoded ID of the event that was acknowledged.
|
||||
pub event_id: String,
|
||||
/// Whether the relay accepted the event.
|
||||
pub accepted: bool,
|
||||
/// Human-readable reason string (empty when accepted without comment).
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Parse a raw relay text frame into a typed [`RelayMessage`].
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn parse_relay_message(text: &str) -> Result<RelayMessage, WsClientError> {
|
||||
let arr: Vec<Value> = serde_json::from_str(text)?;
|
||||
|
||||
let msg_type = arr
|
||||
.first()
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?;
|
||||
|
||||
match msg_type {
|
||||
"EVENT" => {
|
||||
let sub_id = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?
|
||||
.to_string();
|
||||
let event: Event = serde_json::from_value(
|
||||
arr.get(2)
|
||||
.cloned()
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?,
|
||||
)?;
|
||||
Ok(RelayMessage::Event {
|
||||
subscription_id: sub_id,
|
||||
event: Box::new(event),
|
||||
})
|
||||
}
|
||||
"OK" => {
|
||||
let event_id = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?
|
||||
.to_string();
|
||||
let accepted = arr.get(2).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let message = arr
|
||||
.get(3)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
Ok(RelayMessage::Ok(OkResponse {
|
||||
event_id,
|
||||
accepted,
|
||||
message,
|
||||
}))
|
||||
}
|
||||
"EOSE" => {
|
||||
let sub_id = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?
|
||||
.to_string();
|
||||
Ok(RelayMessage::Eose {
|
||||
subscription_id: sub_id,
|
||||
})
|
||||
}
|
||||
"CLOSED" => {
|
||||
let sub_id = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?
|
||||
.to_string();
|
||||
let message = arr
|
||||
.get(2)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
Ok(RelayMessage::Closed {
|
||||
subscription_id: sub_id,
|
||||
message,
|
||||
})
|
||||
}
|
||||
"NOTICE" => {
|
||||
let message = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
Ok(RelayMessage::Notice { message })
|
||||
}
|
||||
"AUTH" => {
|
||||
let challenge = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?
|
||||
.to_string();
|
||||
Ok(RelayMessage::Auth { challenge })
|
||||
}
|
||||
"COUNT" => {
|
||||
let sub_id = arr
|
||||
.get(1)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?
|
||||
.to_string();
|
||||
let count = arr
|
||||
.get(2)
|
||||
.and_then(|o| o.get("count"))
|
||||
.and_then(|c| c.as_u64())
|
||||
.ok_or_else(|| WsClientError::UnexpectedMessage(text.to_string()))?;
|
||||
Ok(RelayMessage::Count {
|
||||
subscription_id: sub_id,
|
||||
count,
|
||||
})
|
||||
}
|
||||
other => Err(WsClientError::UnexpectedMessage(format!(
|
||||
"unknown message type: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a NIP-42 AUTH event, optionally injecting a NIP-OA auth tag.
|
||||
///
|
||||
/// The `auth_tag` parameter allows callers to attach a workspace-scoped
|
||||
/// authorization tag (e.g. `["auth", "<token>"]`) alongside the standard
|
||||
/// relay and challenge tags required by NIP-42.
|
||||
pub fn build_auth_event(
|
||||
challenge: &str,
|
||||
relay_url: &str,
|
||||
keys: &Keys,
|
||||
auth_tag: Option<&Tag>,
|
||||
) -> Result<Event, WsClientError> {
|
||||
let url = RelayUrl::parse(relay_url).map_err(|e| WsClientError::Url(e.to_string()))?;
|
||||
let builder = EventBuilder::auth(challenge, url);
|
||||
let builder = if let Some(tag) = auth_tag {
|
||||
builder.tags([tag.clone()])
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
builder
|
||||
.sign_with_keys(keys)
|
||||
.map_err(|e| WsClientError::EventBuilder(e.to_string()))
|
||||
}
|
||||
Reference in New Issue
Block a user