1use thiserror::Error;
13
14use crate::event::{Alphabet, Event, EventBuilder, Kind, SingleLetterTag, Tag, TagKind};
15use crate::key::{PublicKey, PublicKeyError};
16use crate::types::{RelayUrl, RelayUrlError};
17
18pub const KIND_PUBLIC_MESSAGE: Kind = Kind::PUBLIC_MESSAGE;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PublicMessageRecipient {
24 pub pubkey: PublicKey,
26 pub relay_hint: Option<RelayUrl>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PublicMessage {
33 pub content: String,
35 pub recipients: Vec<PublicMessageRecipient>,
37 pub extra_tags: Vec<Tag>,
39}
40
41#[derive(Debug, Error)]
43#[non_exhaustive]
44pub enum PublicMessageError {
45 #[error("unexpected kind for NIP-A4 public message: {}", .0.as_u16())]
47 WrongKind(Kind),
48 #[error("`p` tag missing recipient pubkey")]
50 MalformedRecipient,
51 #[error("NIP-A4 public message MUST NOT include `e` tags")]
53 ForbiddenEventTag,
54 #[error("NIP-A4 public message has no recipients")]
56 MissingRecipient,
57 #[error(transparent)]
59 InvalidPublicKey(#[from] PublicKeyError),
60 #[error(transparent)]
62 InvalidRelayUrl(#[from] RelayUrlError),
63}
64
65impl PublicMessageRecipient {
66 #[must_use]
68 pub const fn new(pubkey: PublicKey) -> Self {
69 Self {
70 pubkey,
71 relay_hint: None,
72 }
73 }
74
75 #[must_use]
77 pub fn relay_hint(mut self, relay: RelayUrl) -> Self {
78 self.relay_hint = Some(relay);
79 self
80 }
81
82 fn to_tag(&self) -> Tag {
83 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
84 self.relay_hint.as_ref().map_or_else(
85 || Tag::with(&head, [self.pubkey.to_hex()]),
86 |relay| Tag::with(&head, [self.pubkey.to_hex(), relay.as_str().to_owned()]),
87 )
88 }
89
90 fn from_tag(tag: &Tag) -> Result<Self, PublicMessageError> {
91 let pk_hex = tag.get(1).ok_or(PublicMessageError::MalformedRecipient)?;
92 let pubkey = PublicKey::parse(pk_hex)?;
93 let relay_hint = match tag.get(2) {
94 Some(s) if !s.is_empty() => Some(RelayUrl::parse(s)?),
95 _ => None,
96 };
97 Ok(Self { pubkey, relay_hint })
98 }
99}
100
101impl PublicMessage {
102 #[must_use]
104 pub fn new(content: impl Into<String>, recipients: Vec<PublicMessageRecipient>) -> Self {
105 Self {
106 content: content.into(),
107 recipients,
108 extra_tags: Vec::new(),
109 }
110 }
111
112 pub fn from_event(event: &Event) -> Result<Self, PublicMessageError> {
120 if event.kind != KIND_PUBLIC_MESSAGE {
121 return Err(PublicMessageError::WrongKind(event.kind));
122 }
123 let mut recipients: Vec<PublicMessageRecipient> = Vec::new();
124 let mut extra_tags: Vec<Tag> = Vec::new();
125 for tag in &event.tags {
126 match tag.kind() {
127 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
128 recipients.push(PublicMessageRecipient::from_tag(tag)?);
129 }
130 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
131 return Err(PublicMessageError::ForbiddenEventTag);
132 }
133 _ => extra_tags.push(tag.clone()),
134 }
135 }
136 if recipients.is_empty() {
137 return Err(PublicMessageError::MissingRecipient);
138 }
139 Ok(Self {
140 content: event.content.clone(),
141 recipients,
142 extra_tags,
143 })
144 }
145}
146
147impl EventBuilder {
148 pub fn public_message(msg: &PublicMessage) -> Result<Self, PublicMessageError> {
155 if msg.recipients.is_empty() {
156 return Err(PublicMessageError::MissingRecipient);
157 }
158 let mut builder = Self::new(KIND_PUBLIC_MESSAGE, msg.content.clone());
159 for recipient in &msg.recipients {
160 builder = builder.tag(recipient.to_tag());
161 }
162 for tag in &msg.extra_tags {
163 builder = builder.tag(tag.clone());
164 }
165 Ok(builder)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::Keys;
173
174 fn keys() -> Keys {
175 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
176 }
177
178 #[test]
179 fn public_message_round_trip() {
180 let recipient = PublicMessageRecipient::new(*keys().public_key())
181 .relay_hint(RelayUrl::parse("wss://relay.example/").unwrap());
182 let msg = PublicMessage::new("hello", vec![recipient]);
183 let event = EventBuilder::public_message(&msg)
184 .unwrap()
185 .sign_with_keys(&keys())
186 .unwrap();
187 let parsed = PublicMessage::from_event(&event).unwrap();
188 assert_eq!(parsed, msg);
189 }
190
191 #[test]
192 fn missing_recipient_is_rejected() {
193 let msg = PublicMessage::new("hello", Vec::new());
194 assert!(matches!(
195 EventBuilder::public_message(&msg),
196 Err(PublicMessageError::MissingRecipient)
197 ));
198 }
199
200 #[test]
201 fn event_tag_is_rejected() {
202 use crate::event::EventId;
203 let event = EventBuilder::new(KIND_PUBLIC_MESSAGE, "rough")
204 .tag(Tag::e(EventId::from_byte_array([0x99; 32])))
205 .tag(Tag::p(*keys().public_key()))
206 .sign_with_keys(&keys())
207 .unwrap();
208 assert!(matches!(
209 PublicMessage::from_event(&event),
210 Err(PublicMessageError::ForbiddenEventTag)
211 ));
212 }
213
214 #[test]
215 fn wrong_kind_is_rejected() {
216 let event = EventBuilder::text_note("nope")
218 .sign_with_keys(&keys())
219 .unwrap();
220 assert!(matches!(
221 PublicMessage::from_event(&event),
222 Err(PublicMessageError::WrongKind(_))
223 ));
224 }
225
226 #[test]
227 fn extra_tags_round_trip_intact() {
228 let recipient = PublicMessageRecipient::new(*keys().public_key());
231 let mut msg = PublicMessage::new("hi", vec![recipient]);
232 msg.extra_tags.push(Tag::with(
233 &TagKind::from_wire("vendor-foo"),
234 ["bar".to_owned()],
235 ));
236 let event = EventBuilder::public_message(&msg)
237 .unwrap()
238 .sign_with_keys(&keys())
239 .unwrap();
240 let parsed = PublicMessage::from_event(&event).unwrap();
241 assert_eq!(parsed.extra_tags.len(), 1);
242 assert_eq!(parsed.extra_tags[0].name(), "vendor-foo");
243 }
244}