1use thiserror::Error;
11
12use crate::event::{
13 Alphabet, Event, EventBuilder, EventId, EventIdError, Kind, SingleLetterTag, Tag, TagKind,
14};
15use crate::key::{PublicKey, PublicKeyError};
16use crate::types::{RelayUrl, RelayUrlError};
17
18pub const KIND_CHAT_MESSAGE: Kind = Kind::CHAT_MESSAGE;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ChatMessage {
24 pub content: String,
26 pub quote: Option<ChatQuote>,
28 pub extra_tags: Vec<Tag>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ChatQuote {
35 pub id: EventId,
37 pub relay_hint: Option<RelayUrl>,
39 pub author: Option<PublicKey>,
41}
42
43#[derive(Debug, Error)]
45#[non_exhaustive]
46pub enum ChatError {
47 #[error("unexpected kind for NIP-C7 chat message: {}", .0.as_u16())]
49 WrongKind(Kind),
50 #[error("`q` tag missing parent event id")]
52 MalformedQuote,
53 #[error(transparent)]
55 InvalidEventId(#[from] EventIdError),
56 #[error(transparent)]
58 InvalidRelayUrl(#[from] RelayUrlError),
59 #[error(transparent)]
61 InvalidPublicKey(#[from] PublicKeyError),
62}
63
64impl ChatMessage {
65 #[must_use]
67 pub fn new(content: impl Into<String>) -> Self {
68 Self {
69 content: content.into(),
70 quote: None,
71 extra_tags: Vec::new(),
72 }
73 }
74
75 #[must_use]
77 pub fn quote(mut self, quote: ChatQuote) -> Self {
78 self.quote = Some(quote);
79 self
80 }
81
82 pub fn from_event(event: &Event) -> Result<Self, ChatError> {
88 if event.kind != KIND_CHAT_MESSAGE {
89 return Err(ChatError::WrongKind(event.kind));
90 }
91 let mut quote: Option<ChatQuote> = None;
92 let mut extra_tags: Vec<Tag> = Vec::new();
93 for tag in &event.tags {
94 match tag.kind() {
95 TagKind::SingleLetter(s)
96 if !s.uppercase && s.character == Alphabet::Q && quote.is_none() =>
97 {
98 quote = Some(parse_quote(tag)?);
99 }
100 _ => extra_tags.push(tag.clone()),
101 }
102 }
103 Ok(Self {
104 content: event.content.clone(),
105 quote,
106 extra_tags,
107 })
108 }
109}
110
111fn parse_quote(tag: &Tag) -> Result<ChatQuote, ChatError> {
112 let id = tag
113 .get(1)
114 .ok_or(ChatError::MalformedQuote)
115 .and_then(|s| EventId::parse(s).map_err(Into::into))?;
116 let relay_hint = match tag.get(2) {
117 Some(s) if !s.is_empty() => Some(RelayUrl::parse(s)?),
118 _ => None,
119 };
120 let author = match tag.get(3) {
121 Some(s) if !s.is_empty() => Some(PublicKey::parse(s)?),
122 _ => None,
123 };
124 Ok(ChatQuote {
125 id,
126 relay_hint,
127 author,
128 })
129}
130
131fn quote_tag(quote: &ChatQuote) -> Tag {
132 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::Q));
133 let relay = quote
134 .relay_hint
135 .as_ref()
136 .map_or_else(String::new, |r| r.as_str().to_owned());
137 match (quote.author, quote.relay_hint.is_some()) {
138 (Some(pk), _) => Tag::with(&head, [quote.id.to_hex(), relay, pk.to_hex()]),
139 (None, true) => Tag::with(&head, [quote.id.to_hex(), relay]),
140 (None, false) => Tag::with(&head, [quote.id.to_hex()]),
141 }
142}
143
144impl EventBuilder {
145 #[must_use]
147 pub fn chat_message(msg: &ChatMessage) -> Self {
148 let mut builder = Self::new(KIND_CHAT_MESSAGE, msg.content.clone());
149 if let Some(quote) = &msg.quote {
150 builder = builder.tag(quote_tag(quote));
151 }
152 for tag in &msg.extra_tags {
153 builder = builder.tag(tag.clone());
154 }
155 builder
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::Keys;
163
164 fn keys() -> Keys {
165 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
166 }
167
168 #[test]
169 fn chat_message_round_trip() {
170 let quote = ChatQuote {
171 id: EventId::from_byte_array([0x55; 32]),
172 relay_hint: Some(RelayUrl::parse("wss://relay.example/").unwrap()),
173 author: Some(*keys().public_key()),
174 };
175 let msg = ChatMessage::new("yes").quote(quote.clone());
176 let event = EventBuilder::chat_message(&msg)
177 .sign_with_keys(&keys())
178 .unwrap();
179 let parsed = ChatMessage::from_event(&event).unwrap();
180 assert_eq!(parsed, msg);
181 assert_eq!(parsed.quote, Some(quote));
182 }
183
184 #[test]
185 fn top_level_chat_round_trip() {
186 let msg = ChatMessage::new("GM");
187 let event = EventBuilder::chat_message(&msg)
188 .sign_with_keys(&keys())
189 .unwrap();
190 let parsed = ChatMessage::from_event(&event).unwrap();
191 assert_eq!(parsed, msg);
192 assert!(parsed.quote.is_none());
193 }
194
195 #[test]
196 fn wrong_kind_is_rejected() {
197 let event = EventBuilder::text_note("nope")
198 .sign_with_keys(&keys())
199 .unwrap();
200 assert!(matches!(
201 ChatMessage::from_event(&event),
202 Err(ChatError::WrongKind(_))
203 ));
204 }
205}