use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
use crate::event::{Event, EventBuilder, Kind, Tag, TagKind, Tags};
use crate::types::{RelayUrl, RelayUrlError};
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum RelayMarker {
#[default]
ReadWrite,
Read,
Write,
}
impl RelayMarker {
#[must_use]
pub const fn as_wire(self) -> Option<&'static str> {
match self {
Self::ReadWrite => None,
Self::Read => Some("read"),
Self::Write => Some("write"),
}
}
#[must_use]
pub const fn is_read(self) -> bool {
matches!(self, Self::Read | Self::ReadWrite)
}
#[must_use]
pub const fn is_write(self) -> bool {
matches!(self, Self::Write | Self::ReadWrite)
}
}
impl fmt::Display for RelayMarker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_wire().unwrap_or("read+write"))
}
}
impl FromStr for RelayMarker {
type Err = RelayMarkerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"read" => Ok(Self::Read),
"write" => Ok(Self::Write),
other => Err(RelayMarkerError::Unknown(other.to_owned())),
}
}
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum RelayMarkerError {
#[error("unknown NIP-65 relay marker `{0}`")]
Unknown(String),
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum RelayListError {
#[error("expected kind {expected}, got {got}")]
UnexpectedKind {
expected: u16,
got: u16,
},
#[error("`r` tag is missing the relay URL")]
MissingRelayUrl,
#[error(transparent)]
InvalidRelayUrl(#[from] RelayUrlError),
#[error(transparent)]
InvalidMarker(#[from] RelayMarkerError),
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct RelayList {
relays: BTreeMap<RelayUrl, RelayMarker>,
}
impl RelayList {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, url: RelayUrl, marker: RelayMarker) -> Option<RelayMarker> {
self.relays.insert(url, marker)
}
pub fn remove(&mut self, url: &RelayUrl) -> Option<RelayMarker> {
self.relays.remove(url)
}
#[must_use]
pub fn get(&self, url: &RelayUrl) -> Option<RelayMarker> {
self.relays.get(url).copied()
}
#[must_use]
pub fn contains(&self, url: &RelayUrl) -> bool {
self.relays.contains_key(url)
}
#[must_use]
pub fn len(&self) -> usize {
self.relays.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.relays.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&RelayUrl, RelayMarker)> {
self.relays.iter().map(|(url, marker)| (url, *marker))
}
pub fn read_relays(&self) -> impl Iterator<Item = &RelayUrl> {
self.iter().filter(|(_, m)| m.is_read()).map(|(url, _)| url)
}
pub fn write_relays(&self) -> impl Iterator<Item = &RelayUrl> {
self.iter()
.filter(|(_, m)| m.is_write())
.map(|(url, _)| url)
}
#[must_use]
pub fn to_tags(&self) -> Tags {
let tags = self
.relays
.iter()
.map(|(url, marker)| build_r_tag(url, *marker))
.collect::<Vec<_>>();
Tags::from_vec(tags)
}
#[must_use]
pub fn to_event_builder(&self) -> EventBuilder {
EventBuilder::new(Kind::RELAY_LIST, "").tags(self.to_tags())
}
pub fn from_event(event: &Event) -> Result<Self, RelayListError> {
if event.kind != Kind::RELAY_LIST {
return Err(RelayListError::UnexpectedKind {
expected: Kind::RELAY_LIST.as_u16(),
got: event.kind.as_u16(),
});
}
let mut list = Self::new();
for tag in &event.tags {
if !is_relay_tag(&tag.kind()) {
continue;
}
let mut args = tag.values().iter().skip(1);
let url_str = args.next().ok_or(RelayListError::MissingRelayUrl)?;
let url = RelayUrl::parse(url_str)?;
let marker = match args.next() {
Some(s) if !s.is_empty() => s.parse::<RelayMarker>()?,
_ => RelayMarker::ReadWrite,
};
list.insert(url, marker);
}
Ok(list)
}
}
fn build_r_tag(url: &RelayUrl, marker: RelayMarker) -> Tag {
let kind = TagKind::from_wire("r");
let mut values = vec![url.as_str().to_owned()];
if let Some(extra) = marker.as_wire() {
values.push(extra.to_owned());
}
Tag::with(&kind, values)
}
fn is_relay_tag(kind: &TagKind) -> bool {
kind.as_str() == "r"
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
fn relay(url: &str) -> RelayUrl {
RelayUrl::parse(url).unwrap()
}
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
#[test]
fn marker_wire_strings() {
assert_eq!(RelayMarker::Read.as_wire(), Some("read"));
assert_eq!(RelayMarker::Write.as_wire(), Some("write"));
assert_eq!(RelayMarker::ReadWrite.as_wire(), None);
}
#[test]
fn marker_parsing() {
assert_eq!("read".parse::<RelayMarker>().unwrap(), RelayMarker::Read);
assert_eq!("write".parse::<RelayMarker>().unwrap(), RelayMarker::Write);
let err = "both".parse::<RelayMarker>().unwrap_err();
assert!(err.to_string().contains("unknown"));
}
#[test]
fn marker_predicates() {
assert!(RelayMarker::ReadWrite.is_read());
assert!(RelayMarker::ReadWrite.is_write());
assert!(RelayMarker::Read.is_read());
assert!(!RelayMarker::Read.is_write());
assert!(!RelayMarker::Write.is_read());
assert!(RelayMarker::Write.is_write());
}
#[test]
fn round_trip_through_event() {
let mut list = RelayList::new();
list.insert(relay("wss://both.example"), RelayMarker::ReadWrite);
list.insert(relay("wss://read.example"), RelayMarker::Read);
list.insert(relay("wss://write.example"), RelayMarker::Write);
let event = list.to_event_builder().sign_with_keys(&keys()).unwrap();
event.verify().unwrap();
assert_eq!(event.kind, Kind::RELAY_LIST);
let parsed = RelayList::from_event(&event).unwrap();
assert_eq!(parsed, list);
}
#[test]
fn unknown_tags_are_ignored() {
let event = EventBuilder::new(Kind::RELAY_LIST, "")
.tags([
Tag::new(["r", "wss://relay.example"]).unwrap(),
Tag::new(["alt", "ignored"]).unwrap(),
])
.sign_with_keys(&keys())
.unwrap();
let list = RelayList::from_event(&event).unwrap();
assert_eq!(list.len(), 1);
assert!(list.contains(&relay("wss://relay.example")));
}
#[test]
fn missing_url_is_rejected() {
let event = EventBuilder::new(Kind::RELAY_LIST, "")
.tag(Tag::new(["r"]).unwrap())
.sign_with_keys(&keys())
.unwrap();
let err = RelayList::from_event(&event).unwrap_err();
assert!(matches!(err, RelayListError::MissingRelayUrl));
}
#[test]
fn unknown_marker_is_rejected() {
let event = EventBuilder::new(Kind::RELAY_LIST, "")
.tag(Tag::new(["r", "wss://relay.example", "duplex"]).unwrap())
.sign_with_keys(&keys())
.unwrap();
let err = RelayList::from_event(&event).unwrap_err();
assert!(matches!(
err,
RelayListError::InvalidMarker(RelayMarkerError::Unknown(_))
));
}
#[test]
fn wrong_kind_is_rejected() {
let event = EventBuilder::text_note("not a relay list")
.sign_with_keys(&keys())
.unwrap();
let err = RelayList::from_event(&event).unwrap_err();
assert!(matches!(
err,
RelayListError::UnexpectedKind {
expected: 10_002,
got: 1
}
));
}
#[test]
fn read_and_write_iterators() {
let mut list = RelayList::new();
list.insert(relay("wss://both.example"), RelayMarker::ReadWrite);
list.insert(relay("wss://read.example"), RelayMarker::Read);
list.insert(relay("wss://write.example"), RelayMarker::Write);
let read: Vec<_> = list.read_relays().collect();
let write: Vec<_> = list.write_relays().collect();
assert_eq!(read.len(), 2);
assert_eq!(write.len(), 2);
assert!(read.contains(&&relay("wss://both.example")));
assert!(read.contains(&&relay("wss://read.example")));
assert!(write.contains(&&relay("wss://both.example")));
assert!(write.contains(&&relay("wss://write.example")));
}
}