use thiserror::Error;
use crate::event::{
Alphabet, Event, EventBuilder, EventId, EventIdError, Kind, SingleLetterTag, Tag, TagKind,
};
use crate::key::{PublicKey, PublicKeyError};
use crate::types::{RelayUrl, RelayUrlError};
pub const KIND_CHAT_MESSAGE: Kind = Kind::CHAT_MESSAGE;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChatMessage {
pub content: String,
pub quote: Option<ChatQuote>,
pub extra_tags: Vec<Tag>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChatQuote {
pub id: EventId,
pub relay_hint: Option<RelayUrl>,
pub author: Option<PublicKey>,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ChatError {
#[error("unexpected kind for NIP-C7 chat message: {}", .0.as_u16())]
WrongKind(Kind),
#[error("`q` tag missing parent event id")]
MalformedQuote,
#[error(transparent)]
InvalidEventId(#[from] EventIdError),
#[error(transparent)]
InvalidRelayUrl(#[from] RelayUrlError),
#[error(transparent)]
InvalidPublicKey(#[from] PublicKeyError),
}
impl ChatMessage {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
quote: None,
extra_tags: Vec::new(),
}
}
#[must_use]
pub fn quote(mut self, quote: ChatQuote) -> Self {
self.quote = Some(quote);
self
}
pub fn from_event(event: &Event) -> Result<Self, ChatError> {
if event.kind != KIND_CHAT_MESSAGE {
return Err(ChatError::WrongKind(event.kind));
}
let mut quote: Option<ChatQuote> = None;
let mut extra_tags: Vec<Tag> = Vec::new();
for tag in &event.tags {
match tag.kind() {
TagKind::SingleLetter(s)
if !s.uppercase && s.character == Alphabet::Q && quote.is_none() =>
{
quote = Some(parse_quote(tag)?);
}
_ => extra_tags.push(tag.clone()),
}
}
Ok(Self {
content: event.content.clone(),
quote,
extra_tags,
})
}
}
fn parse_quote(tag: &Tag) -> Result<ChatQuote, ChatError> {
let id = tag
.get(1)
.ok_or(ChatError::MalformedQuote)
.and_then(|s| EventId::parse(s).map_err(Into::into))?;
let relay_hint = match tag.get(2) {
Some(s) if !s.is_empty() => Some(RelayUrl::parse(s)?),
_ => None,
};
let author = match tag.get(3) {
Some(s) if !s.is_empty() => Some(PublicKey::parse(s)?),
_ => None,
};
Ok(ChatQuote {
id,
relay_hint,
author,
})
}
fn quote_tag(quote: &ChatQuote) -> Tag {
let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::Q));
let relay = quote
.relay_hint
.as_ref()
.map_or_else(String::new, |r| r.as_str().to_owned());
match (quote.author, quote.relay_hint.is_some()) {
(Some(pk), _) => Tag::with(&head, [quote.id.to_hex(), relay, pk.to_hex()]),
(None, true) => Tag::with(&head, [quote.id.to_hex(), relay]),
(None, false) => Tag::with(&head, [quote.id.to_hex()]),
}
}
impl EventBuilder {
#[must_use]
pub fn chat_message(msg: &ChatMessage) -> Self {
let mut builder = Self::new(KIND_CHAT_MESSAGE, msg.content.clone());
if let Some(quote) = &msg.quote {
builder = builder.tag(quote_tag(quote));
}
for tag in &msg.extra_tags {
builder = builder.tag(tag.clone());
}
builder
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
#[test]
fn chat_message_round_trip() {
let quote = ChatQuote {
id: EventId::from_byte_array([0x55; 32]),
relay_hint: Some(RelayUrl::parse("wss://relay.example/").unwrap()),
author: Some(*keys().public_key()),
};
let msg = ChatMessage::new("yes").quote(quote.clone());
let event = EventBuilder::chat_message(&msg)
.sign_with_keys(&keys())
.unwrap();
let parsed = ChatMessage::from_event(&event).unwrap();
assert_eq!(parsed, msg);
assert_eq!(parsed.quote, Some(quote));
}
#[test]
fn top_level_chat_round_trip() {
let msg = ChatMessage::new("GM");
let event = EventBuilder::chat_message(&msg)
.sign_with_keys(&keys())
.unwrap();
let parsed = ChatMessage::from_event(&event).unwrap();
assert_eq!(parsed, msg);
assert!(parsed.quote.is_none());
}
#[test]
fn wrong_kind_is_rejected() {
let event = EventBuilder::text_note("nope")
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
ChatMessage::from_event(&event),
Err(ChatError::WrongKind(_))
));
}
}