1use thiserror::Error;
10
11use crate::event::{Event, EventBuilder, Kind, Tag, TagKind};
12
13pub const KIND_THREAD: Kind = Kind::THREAD;
15
16const TITLE_TAG: &str = "title";
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Thread {
21 pub content: String,
23 pub title: Option<String>,
25 pub extra_tags: Vec<Tag>,
27}
28
29#[derive(Debug, Error)]
31#[non_exhaustive]
32pub enum ThreadError {
33 #[error("unexpected kind for NIP-7D thread: {}", .0.as_u16())]
35 WrongKind(Kind),
36}
37
38impl Thread {
39 #[must_use]
41 pub fn new(content: impl Into<String>) -> Self {
42 Self {
43 content: content.into(),
44 title: None,
45 extra_tags: Vec::new(),
46 }
47 }
48
49 #[must_use]
51 pub fn title(mut self, title: impl Into<String>) -> Self {
52 self.title = Some(title.into());
53 self
54 }
55
56 pub fn from_event(event: &Event) -> Result<Self, ThreadError> {
62 if event.kind != KIND_THREAD {
63 return Err(ThreadError::WrongKind(event.kind));
64 }
65 let mut title: Option<String> = None;
66 let mut extra_tags: Vec<Tag> = Vec::new();
67 for tag in &event.tags {
68 if tag.name() == TITLE_TAG && title.is_none() {
69 title = tag.get(1).map(str::to_owned);
70 } else {
71 extra_tags.push(tag.clone());
72 }
73 }
74 Ok(Self {
75 content: event.content.clone(),
76 title,
77 extra_tags,
78 })
79 }
80}
81
82impl EventBuilder {
83 #[must_use]
85 pub fn thread(thread: &Thread) -> Self {
86 let mut builder = Self::new(KIND_THREAD, thread.content.clone());
87 if let Some(title) = &thread.title {
88 builder = builder.tag(Tag::with(&TagKind::from_wire(TITLE_TAG), [title.clone()]));
89 }
90 for tag in &thread.extra_tags {
91 builder = builder.tag(tag.clone());
92 }
93 builder
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::Keys;
101
102 fn keys() -> Keys {
103 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
104 }
105
106 #[test]
107 fn thread_round_trip() {
108 let thread = Thread::new("Good morning").title("GM");
109 let event = EventBuilder::thread(&thread)
110 .sign_with_keys(&keys())
111 .unwrap();
112 let parsed = Thread::from_event(&event).unwrap();
113 assert_eq!(parsed, thread);
114 }
115
116 #[test]
117 fn thread_without_title() {
118 let thread = Thread::new("orphan");
119 let event = EventBuilder::thread(&thread)
120 .sign_with_keys(&keys())
121 .unwrap();
122 let parsed = Thread::from_event(&event).unwrap();
123 assert!(parsed.title.is_none());
124 }
125
126 #[test]
127 fn wrong_kind_is_rejected() {
128 let event = EventBuilder::text_note("nope")
129 .sign_with_keys(&keys())
130 .unwrap();
131 assert!(matches!(
132 Thread::from_event(&event),
133 Err(ThreadError::WrongKind(_))
134 ));
135 }
136}