1use std::collections::BTreeMap;
38use std::fmt;
39use std::str::FromStr;
40
41use thiserror::Error;
42
43use crate::event::{Event, EventBuilder, Kind, Tag, TagKind, Tags};
44use crate::types::{RelayUrl, RelayUrlError};
45
46#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48#[non_exhaustive]
49pub enum RelayMarker {
50 #[default]
53 ReadWrite,
54 Read,
56 Write,
58}
59
60impl RelayMarker {
61 #[must_use]
65 pub const fn as_wire(self) -> Option<&'static str> {
66 match self {
67 Self::ReadWrite => None,
68 Self::Read => Some("read"),
69 Self::Write => Some("write"),
70 }
71 }
72
73 #[must_use]
75 pub const fn is_read(self) -> bool {
76 matches!(self, Self::Read | Self::ReadWrite)
77 }
78
79 #[must_use]
81 pub const fn is_write(self) -> bool {
82 matches!(self, Self::Write | Self::ReadWrite)
83 }
84}
85
86impl fmt::Display for RelayMarker {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(self.as_wire().unwrap_or("read+write"))
89 }
90}
91
92impl FromStr for RelayMarker {
93 type Err = RelayMarkerError;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 match s {
97 "read" => Ok(Self::Read),
98 "write" => Ok(Self::Write),
99 other => Err(RelayMarkerError::Unknown(other.to_owned())),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Error)]
106#[non_exhaustive]
107pub enum RelayMarkerError {
108 #[error("unknown NIP-65 relay marker `{0}`")]
110 Unknown(String),
111}
112
113#[derive(Debug, Clone, Error)]
115#[non_exhaustive]
116pub enum RelayListError {
117 #[error("expected kind {expected}, got {got}")]
119 UnexpectedKind {
120 expected: u16,
122 got: u16,
124 },
125 #[error("`r` tag is missing the relay URL")]
127 MissingRelayUrl,
128 #[error(transparent)]
130 InvalidRelayUrl(#[from] RelayUrlError),
131 #[error(transparent)]
133 InvalidMarker(#[from] RelayMarkerError),
134}
135
136#[derive(Debug, Default, Clone, PartialEq, Eq)]
141pub struct RelayList {
142 relays: BTreeMap<RelayUrl, RelayMarker>,
143}
144
145impl RelayList {
146 #[must_use]
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn insert(&mut self, url: RelayUrl, marker: RelayMarker) -> Option<RelayMarker> {
163 self.relays.insert(url, marker)
164 }
165
166 pub fn remove(&mut self, url: &RelayUrl) -> Option<RelayMarker> {
168 self.relays.remove(url)
169 }
170
171 #[must_use]
173 pub fn get(&self, url: &RelayUrl) -> Option<RelayMarker> {
174 self.relays.get(url).copied()
175 }
176
177 #[must_use]
179 pub fn contains(&self, url: &RelayUrl) -> bool {
180 self.relays.contains_key(url)
181 }
182
183 #[must_use]
185 pub fn len(&self) -> usize {
186 self.relays.len()
187 }
188
189 #[must_use]
191 pub fn is_empty(&self) -> bool {
192 self.relays.is_empty()
193 }
194
195 pub fn iter(&self) -> impl Iterator<Item = (&RelayUrl, RelayMarker)> {
197 self.relays.iter().map(|(url, marker)| (url, *marker))
198 }
199
200 pub fn read_relays(&self) -> impl Iterator<Item = &RelayUrl> {
202 self.iter().filter(|(_, m)| m.is_read()).map(|(url, _)| url)
203 }
204
205 pub fn write_relays(&self) -> impl Iterator<Item = &RelayUrl> {
207 self.iter()
208 .filter(|(_, m)| m.is_write())
209 .map(|(url, _)| url)
210 }
211
212 #[must_use]
215 pub fn to_tags(&self) -> Tags {
216 let tags = self
217 .relays
218 .iter()
219 .map(|(url, marker)| build_r_tag(url, *marker))
220 .collect::<Vec<_>>();
221 Tags::from_vec(tags)
222 }
223
224 #[must_use]
231 pub fn to_event_builder(&self) -> EventBuilder {
232 EventBuilder::new(Kind::RELAY_LIST, "").tags(self.to_tags())
233 }
234
235 pub fn from_event(event: &Event) -> Result<Self, RelayListError> {
245 if event.kind != Kind::RELAY_LIST {
246 return Err(RelayListError::UnexpectedKind {
247 expected: Kind::RELAY_LIST.as_u16(),
248 got: event.kind.as_u16(),
249 });
250 }
251 let mut list = Self::new();
252 for tag in &event.tags {
253 if !is_relay_tag(&tag.kind()) {
254 continue;
255 }
256 let mut args = tag.values().iter().skip(1);
257 let url_str = args.next().ok_or(RelayListError::MissingRelayUrl)?;
258 let url = RelayUrl::parse(url_str)?;
259 let marker = match args.next() {
260 Some(s) if !s.is_empty() => s.parse::<RelayMarker>()?,
261 _ => RelayMarker::ReadWrite,
262 };
263 list.insert(url, marker);
264 }
265 Ok(list)
266 }
267}
268
269fn build_r_tag(url: &RelayUrl, marker: RelayMarker) -> Tag {
270 let kind = TagKind::from_wire("r");
271 let mut values = vec![url.as_str().to_owned()];
272 if let Some(extra) = marker.as_wire() {
273 values.push(extra.to_owned());
274 }
275 Tag::with(&kind, values)
276}
277
278fn is_relay_tag(kind: &TagKind) -> bool {
279 kind.as_str() == "r"
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::Keys;
286
287 fn relay(url: &str) -> RelayUrl {
288 RelayUrl::parse(url).unwrap()
289 }
290
291 fn keys() -> Keys {
292 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
293 }
294
295 #[test]
296 fn marker_wire_strings() {
297 assert_eq!(RelayMarker::Read.as_wire(), Some("read"));
298 assert_eq!(RelayMarker::Write.as_wire(), Some("write"));
299 assert_eq!(RelayMarker::ReadWrite.as_wire(), None);
300 }
301
302 #[test]
303 fn marker_parsing() {
304 assert_eq!("read".parse::<RelayMarker>().unwrap(), RelayMarker::Read);
305 assert_eq!("write".parse::<RelayMarker>().unwrap(), RelayMarker::Write);
306 let err = "both".parse::<RelayMarker>().unwrap_err();
307 assert!(err.to_string().contains("unknown"));
308 }
309
310 #[test]
311 fn marker_predicates() {
312 assert!(RelayMarker::ReadWrite.is_read());
313 assert!(RelayMarker::ReadWrite.is_write());
314 assert!(RelayMarker::Read.is_read());
315 assert!(!RelayMarker::Read.is_write());
316 assert!(!RelayMarker::Write.is_read());
317 assert!(RelayMarker::Write.is_write());
318 }
319
320 #[test]
321 fn round_trip_through_event() {
322 let mut list = RelayList::new();
323 list.insert(relay("wss://both.example"), RelayMarker::ReadWrite);
324 list.insert(relay("wss://read.example"), RelayMarker::Read);
325 list.insert(relay("wss://write.example"), RelayMarker::Write);
326
327 let event = list.to_event_builder().sign_with_keys(&keys()).unwrap();
328 event.verify().unwrap();
329 assert_eq!(event.kind, Kind::RELAY_LIST);
330
331 let parsed = RelayList::from_event(&event).unwrap();
332 assert_eq!(parsed, list);
333 }
334
335 #[test]
336 fn unknown_tags_are_ignored() {
337 let event = EventBuilder::new(Kind::RELAY_LIST, "")
338 .tags([
339 Tag::new(["r", "wss://relay.example"]).unwrap(),
340 Tag::new(["alt", "ignored"]).unwrap(),
341 ])
342 .sign_with_keys(&keys())
343 .unwrap();
344 let list = RelayList::from_event(&event).unwrap();
345 assert_eq!(list.len(), 1);
346 assert!(list.contains(&relay("wss://relay.example")));
347 }
348
349 #[test]
350 fn missing_url_is_rejected() {
351 let event = EventBuilder::new(Kind::RELAY_LIST, "")
352 .tag(Tag::new(["r"]).unwrap())
353 .sign_with_keys(&keys())
354 .unwrap();
355 let err = RelayList::from_event(&event).unwrap_err();
356 assert!(matches!(err, RelayListError::MissingRelayUrl));
357 }
358
359 #[test]
360 fn unknown_marker_is_rejected() {
361 let event = EventBuilder::new(Kind::RELAY_LIST, "")
362 .tag(Tag::new(["r", "wss://relay.example", "duplex"]).unwrap())
363 .sign_with_keys(&keys())
364 .unwrap();
365 let err = RelayList::from_event(&event).unwrap_err();
366 assert!(matches!(
367 err,
368 RelayListError::InvalidMarker(RelayMarkerError::Unknown(_))
369 ));
370 }
371
372 #[test]
373 fn wrong_kind_is_rejected() {
374 let event = EventBuilder::text_note("not a relay list")
375 .sign_with_keys(&keys())
376 .unwrap();
377 let err = RelayList::from_event(&event).unwrap_err();
378 assert!(matches!(
379 err,
380 RelayListError::UnexpectedKind {
381 expected: 10_002,
382 got: 1
383 }
384 ));
385 }
386
387 #[test]
388 fn read_and_write_iterators() {
389 let mut list = RelayList::new();
390 list.insert(relay("wss://both.example"), RelayMarker::ReadWrite);
391 list.insert(relay("wss://read.example"), RelayMarker::Read);
392 list.insert(relay("wss://write.example"), RelayMarker::Write);
393
394 let read: Vec<_> = list.read_relays().collect();
395 let write: Vec<_> = list.write_relays().collect();
396 assert_eq!(read.len(), 2);
397 assert_eq!(write.len(), 2);
398 assert!(read.contains(&&relay("wss://both.example")));
399 assert!(read.contains(&&relay("wss://read.example")));
400 assert!(write.contains(&&relay("wss://both.example")));
401 assert!(write.contains(&&relay("wss://write.example")));
402 }
403}