1use indexmap::IndexMap;
40use serde::{Deserialize, Serialize};
41use thiserror::Error;
42
43use crate::event::{Alphabet, Event, EventBuilder, EventId, Kind, SingleLetterTag, Tag, TagKind};
44use crate::key::PublicKey;
45use crate::types::{RelayUrl, RelayUrlError};
46
47pub const KIND_CHANNEL_CREATE: Kind = Kind::CHANNEL_CREATION;
49pub const KIND_CHANNEL_METADATA: Kind = Kind::CHANNEL_METADATA;
51pub const KIND_CHANNEL_MESSAGE: Kind = Kind::CHANNEL_MESSAGE;
53pub const KIND_CHANNEL_HIDE_MESSAGE: Kind = Kind::CHANNEL_HIDE_MESSAGE;
55pub const KIND_CHANNEL_MUTE_USER: Kind = Kind::CHANNEL_MUTE_USER;
57
58#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ChannelMetadata {
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub name: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub about: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub picture: Option<String>,
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub relays: Vec<RelayUrl>,
80 #[serde(flatten)]
82 pub extra: IndexMap<String, serde_json::Value>,
83}
84
85impl ChannelMetadata {
86 #[must_use]
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 #[must_use]
94 pub fn name(mut self, name: impl Into<String>) -> Self {
95 self.name = Some(name.into());
96 self
97 }
98
99 #[must_use]
101 pub fn about(mut self, about: impl Into<String>) -> Self {
102 self.about = Some(about.into());
103 self
104 }
105
106 #[must_use]
108 pub fn picture(mut self, picture: impl Into<String>) -> Self {
109 self.picture = Some(picture.into());
110 self
111 }
112
113 #[must_use]
115 pub fn relay(mut self, relay: RelayUrl) -> Self {
116 self.relays.push(relay);
117 self
118 }
119
120 #[must_use]
122 pub fn relays<I>(mut self, relays: I) -> Self
123 where
124 I: IntoIterator<Item = RelayUrl>,
125 {
126 self.relays.extend(relays);
127 self
128 }
129
130 pub fn to_json(&self) -> Result<String, serde_json::Error> {
140 serde_json::to_string(self)
141 }
142
143 pub fn from_json(json: &str) -> Result<Self, ChannelMetadataError> {
152 let raw: serde_json::Value =
153 serde_json::from_str(json).map_err(ChannelMetadataError::InvalidJson)?;
154 let serde_json::Value::Object(mut map) = raw else {
155 return Err(ChannelMetadataError::NotAnObject);
156 };
157
158 let mut metadata = Self::default();
159 if let Some(value) = map.remove("name") {
160 metadata.name = string_field(value, "name")?;
161 }
162 if let Some(value) = map.remove("about") {
163 metadata.about = string_field(value, "about")?;
164 }
165 if let Some(value) = map.remove("picture") {
166 metadata.picture = string_field(value, "picture")?;
167 }
168 if let Some(value) = map.remove("relays") {
169 metadata.relays = relays_field(value)?;
170 }
171 for (key, value) in map {
172 metadata.extra.insert(key, value);
173 }
174 Ok(metadata)
175 }
176}
177
178fn string_field(
179 value: serde_json::Value,
180 key: &'static str,
181) -> Result<Option<String>, ChannelMetadataError> {
182 match value {
183 serde_json::Value::Null => Ok(None),
184 serde_json::Value::String(s) => Ok(Some(s)),
185 other => Err(ChannelMetadataError::InvalidStringField {
186 key,
187 actual: type_name(&other).to_owned(),
188 }),
189 }
190}
191
192fn relays_field(value: serde_json::Value) -> Result<Vec<RelayUrl>, ChannelMetadataError> {
193 let serde_json::Value::Array(arr) = value else {
194 return Err(ChannelMetadataError::InvalidRelaysField);
195 };
196 let mut relays: Vec<RelayUrl> = Vec::with_capacity(arr.len());
197 for item in arr {
198 let serde_json::Value::String(url) = item else {
199 return Err(ChannelMetadataError::InvalidRelaysField);
200 };
201 relays.push(RelayUrl::parse(&url).map_err(ChannelMetadataError::InvalidRelayUrl)?);
202 }
203 Ok(relays)
204}
205
206const fn type_name(value: &serde_json::Value) -> &'static str {
207 match value {
208 serde_json::Value::Null => "null",
209 serde_json::Value::Bool(_) => "bool",
210 serde_json::Value::Number(_) => "number",
211 serde_json::Value::String(_) => "string",
212 serde_json::Value::Array(_) => "array",
213 serde_json::Value::Object(_) => "object",
214 }
215}
216
217#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
223pub struct HideReason {
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub reason: Option<String>,
227 #[serde(flatten)]
229 pub extra: IndexMap<String, serde_json::Value>,
230}
231
232impl HideReason {
233 #[must_use]
235 pub fn new() -> Self {
236 Self::default()
237 }
238
239 #[must_use]
241 pub fn reason(mut self, reason: impl Into<String>) -> Self {
242 self.reason = Some(reason.into());
243 self
244 }
245
246 #[must_use]
252 pub fn to_json(&self) -> String {
253 if self.reason.is_none() && self.extra.is_empty() {
254 return String::new();
255 }
256 serde_json::to_string(self).unwrap_or_default()
257 }
258
259 pub fn from_json(json: &str) -> Result<Self, ChannelMetadataError> {
268 if json.trim().is_empty() {
269 return Ok(Self::default());
270 }
271 serde_json::from_str(json).map_err(ChannelMetadataError::InvalidJson)
272 }
273}
274
275#[derive(Debug, Error)]
277#[non_exhaustive]
278pub enum ChannelMetadataError {
279 #[error("invalid JSON: {0}")]
281 InvalidJson(#[source] serde_json::Error),
282 #[error("expected a JSON object at the top level")]
284 NotAnObject,
285 #[error("`{key}` must be a JSON string, got {actual}")]
287 InvalidStringField {
288 key: &'static str,
290 actual: String,
292 },
293 #[error("`relays` must be a JSON array of strings")]
295 InvalidRelaysField,
296 #[error("invalid relay URL: {0}")]
298 InvalidRelayUrl(#[source] RelayUrlError),
299}
300
301impl EventBuilder {
302 pub fn channel_create(metadata: &ChannelMetadata) -> Result<Self, serde_json::Error> {
308 let json = metadata.to_json()?;
309 Ok(Self::new(KIND_CHANNEL_CREATE, json))
310 }
311
312 pub fn channel_metadata_update(
321 metadata: &ChannelMetadata,
322 channel: EventId,
323 relay: Option<&RelayUrl>,
324 ) -> Result<Self, serde_json::Error> {
325 let json = metadata.to_json()?;
326 Ok(Self::new(KIND_CHANNEL_METADATA, json).tag(channel_root_tag(channel, relay)))
327 }
328
329 #[must_use]
332 pub fn channel_message_root(
333 channel: EventId,
334 relay: Option<&RelayUrl>,
335 content: impl Into<String>,
336 ) -> Self {
337 Self::new(KIND_CHANNEL_MESSAGE, content).tag(channel_root_tag(channel, relay))
338 }
339
340 #[must_use]
345 pub fn channel_message_reply(
346 channel: EventId,
347 channel_relay: Option<&RelayUrl>,
348 parent_message: EventId,
349 parent_relay: Option<&RelayUrl>,
350 parent_author: PublicKey,
351 author_relay: Option<&RelayUrl>,
352 content: impl Into<String>,
353 ) -> Self {
354 Self::new(KIND_CHANNEL_MESSAGE, content)
355 .tag(channel_root_tag(channel, channel_relay))
356 .tag(channel_reply_tag(parent_message, parent_relay))
357 .tag(channel_p_tag(parent_author, author_relay))
358 }
359
360 #[must_use]
364 pub fn channel_hide_message(target: EventId, reason: Option<&HideReason>) -> Self {
365 let body = reason.map(HideReason::to_json).unwrap_or_default();
366 Self::new(KIND_CHANNEL_HIDE_MESSAGE, body).tag(channel_e_tag(target))
367 }
368
369 #[must_use]
373 pub fn channel_mute_user(target: PublicKey, reason: Option<&HideReason>) -> Self {
374 let body = reason.map(HideReason::to_json).unwrap_or_default();
375 Self::new(KIND_CHANNEL_MUTE_USER, body).tag(channel_p_tag(target, None))
376 }
377}
378
379fn channel_root_tag(channel: EventId, relay: Option<&RelayUrl>) -> Tag {
380 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
381 let mut values: Vec<String> = Vec::with_capacity(4);
382 values.push(channel.to_hex());
383 values.push(relay.map(|r| r.as_str().to_owned()).unwrap_or_default());
384 values.push("root".to_owned());
385 Tag::with(&head, values)
386}
387
388fn channel_reply_tag(parent: EventId, relay: Option<&RelayUrl>) -> Tag {
389 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
390 let mut values: Vec<String> = Vec::with_capacity(4);
391 values.push(parent.to_hex());
392 values.push(relay.map(|r| r.as_str().to_owned()).unwrap_or_default());
393 values.push("reply".to_owned());
394 Tag::with(&head, values)
395}
396
397fn channel_e_tag(target: EventId) -> Tag {
398 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
399 Tag::with(&head, [target.to_hex()])
400}
401
402fn channel_p_tag(target: PublicKey, relay: Option<&RelayUrl>) -> Tag {
403 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
404 let mut values: Vec<String> = Vec::with_capacity(2);
405 values.push(target.to_hex());
406 if let Some(relay) = relay {
407 values.push(relay.as_str().to_owned());
408 }
409 Tag::with(&head, values)
410}
411
412#[must_use]
415pub fn channel_root_id(event: &Event) -> Option<EventId> {
416 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
417 for tag in event.tags.find_all(&head) {
418 if tag.get(3) == Some("root")
419 && let Some(id_hex) = tag.get(1)
420 && let Ok(id) = EventId::parse(id_hex)
421 {
422 return Some(id);
423 }
424 }
425 None
426}
427
428#[must_use]
431pub fn channel_reply_id(event: &Event) -> Option<EventId> {
432 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
433 for tag in event.tags.find_all(&head) {
434 if tag.get(3) == Some("reply")
435 && let Some(id_hex) = tag.get(1)
436 && let Ok(id) = EventId::parse(id_hex)
437 {
438 return Some(id);
439 }
440 }
441 None
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::Keys;
448
449 fn keys() -> Keys {
450 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
451 }
452
453 fn event_id_zero() -> EventId {
454 EventId::from_byte_array([0x42; 32])
455 }
456
457 fn event_id_one() -> EventId {
458 EventId::from_byte_array([0xab; 32])
459 }
460
461 #[test]
462 fn channel_metadata_round_trips_through_json() {
463 let metadata = ChannelMetadata::new()
464 .name("Demo")
465 .about("desc")
466 .picture("https://example.com/p.png")
467 .relays([
468 RelayUrl::parse("wss://nos.lol").unwrap(),
469 RelayUrl::parse("wss://relay.example/").unwrap(),
470 ]);
471 let json = metadata.to_json().unwrap();
472 let parsed = ChannelMetadata::from_json(&json).unwrap();
473 assert_eq!(parsed, metadata);
474 }
475
476 #[test]
477 fn channel_metadata_extra_fields_round_trip() {
478 let json = r#"{"name":"X","custom":[1,2,3]}"#;
479 let parsed = ChannelMetadata::from_json(json).unwrap();
480 assert_eq!(parsed.name.as_deref(), Some("X"));
481 assert_eq!(
482 parsed
483 .extra
484 .get("custom")
485 .and_then(|v| v.as_array())
486 .map(Vec::len),
487 Some(3),
488 );
489 }
490
491 #[test]
492 fn channel_metadata_rejects_non_string_picture() {
493 let json = r#"{"picture":123}"#;
494 let err = ChannelMetadata::from_json(json).unwrap_err();
495 assert!(matches!(
496 err,
497 ChannelMetadataError::InvalidStringField { key: "picture", .. }
498 ));
499 }
500
501 #[test]
502 fn channel_metadata_rejects_invalid_relay_url() {
503 let json = r#"{"relays":["not-a-relay"]}"#;
504 let err = ChannelMetadata::from_json(json).unwrap_err();
505 assert!(matches!(err, ChannelMetadataError::InvalidRelayUrl(_)));
506 }
507
508 #[test]
509 fn channel_create_emits_kind_40_with_metadata_in_content() {
510 let metadata = ChannelMetadata::new().name("hello");
511 let event = EventBuilder::channel_create(&metadata)
512 .unwrap()
513 .sign_with_keys(&keys())
514 .unwrap();
515 assert_eq!(event.kind, KIND_CHANNEL_CREATE);
516 let parsed = ChannelMetadata::from_json(&event.content).unwrap();
517 assert_eq!(parsed.name.as_deref(), Some("hello"));
518 }
519
520 #[test]
521 fn channel_metadata_update_includes_root_marker() {
522 let metadata = ChannelMetadata::new().name("upd");
523 let channel = event_id_zero();
524 let event = EventBuilder::channel_metadata_update(&metadata, channel, None)
525 .unwrap()
526 .sign_with_keys(&keys())
527 .unwrap();
528 assert_eq!(event.kind, KIND_CHANNEL_METADATA);
529 assert_eq!(channel_root_id(&event), Some(channel));
530 }
531
532 #[test]
533 fn channel_message_root_only_carries_one_e_tag() {
534 let channel = event_id_zero();
535 let event = EventBuilder::channel_message_root(channel, None, "hello")
536 .sign_with_keys(&keys())
537 .unwrap();
538 assert_eq!(event.kind, KIND_CHANNEL_MESSAGE);
539 assert_eq!(channel_root_id(&event), Some(channel));
540 assert_eq!(channel_reply_id(&event), None);
541 }
542
543 #[test]
544 fn channel_message_reply_carries_root_reply_and_p() {
545 let channel = event_id_zero();
546 let parent = event_id_one();
547 let parent_author = *keys().public_key();
548 let event = EventBuilder::channel_message_reply(
549 channel,
550 None,
551 parent,
552 None,
553 parent_author,
554 None,
555 "yo",
556 )
557 .sign_with_keys(&keys())
558 .unwrap();
559 assert_eq!(channel_root_id(&event), Some(channel));
560 assert_eq!(channel_reply_id(&event), Some(parent));
561 }
562
563 #[test]
564 fn hide_message_with_reason_emits_json_content() {
565 let reason = HideReason::new().reason("dick pic");
566 let event = EventBuilder::channel_hide_message(event_id_zero(), Some(&reason))
567 .sign_with_keys(&keys())
568 .unwrap();
569 assert_eq!(event.kind, KIND_CHANNEL_HIDE_MESSAGE);
570 assert!(event.content.contains("\"reason\":\"dick pic\""));
571 }
572
573 #[test]
574 fn hide_message_without_reason_has_empty_content() {
575 let event = EventBuilder::channel_hide_message(event_id_zero(), None)
576 .sign_with_keys(&keys())
577 .unwrap();
578 assert_eq!(event.content, "");
579 }
580
581 #[test]
582 fn mute_user_carries_p_tag_for_target() {
583 let target = *keys().public_key();
584 let event = EventBuilder::channel_mute_user(target, None)
585 .sign_with_keys(&keys())
586 .unwrap();
587 assert_eq!(event.kind, KIND_CHANNEL_MUTE_USER);
588 let p_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
589 let p_tag = event.tags.find_first(&p_kind).unwrap();
590 assert_eq!(p_tag.get(1), Some(target.to_hex().as_str()));
591 }
592
593 #[test]
594 fn hide_reason_round_trips_extra_fields() {
595 let json = r#"{"reason":"x","custom":42}"#;
596 let parsed = HideReason::from_json(json).unwrap();
597 assert_eq!(parsed.reason.as_deref(), Some("x"));
598 assert_eq!(
599 parsed
600 .extra
601 .get("custom")
602 .and_then(serde_json::Value::as_i64),
603 Some(42),
604 );
605 }
606}