Skip to content

Conversation

SubatomicPlanets
Copy link
Contributor

nostr: add NIP-C7 support

Added a new ChatMessage kind and 2 new builders: chat_message and chat_message_reply.
The code is structured similarly to text_note and text_note_reply.

Copy link
Contributor

@TheAwiteb TheAwiteb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also update crates/nostr/CHANGELOG.md

@TheAwiteb
Copy link
Contributor

TheAwiteb commented Sep 7, 2025

Also I think you should parse the content using NostrParser and return an error if it doesn't contains the quoted event.

Or maybe add it if it doesn't contains it? the NIP-C7 shows it should be the first line. I'm not sure.

@yukibtc what do you think?

@SubatomicPlanets
Copy link
Contributor Author

Okay @TheAwiteb, I updated the changelog and code. I am new to Rust and Nostr so thanks for the help. I think it should automatically add the event to the context if it's not there already. I played around with NostrParser a bit but I think something like this could be simpler:

pub fn chat_message_reply<S>(content: S, reply_to: &Event, relay_url: Option<RelayUrl>) -> Self
where
    S: Into<String>,
{
    let mut content_string: String = content.into();

    let nevent = Nip19Event {
        event_id: reply_to.id,
        author: None,
        kind: None,
        relays: vec![],
    }.to_bech32().unwrap();
	
    if !content_string.starts_with(&format!("nostr:{}", nevent)) {
        content_string = format!("nostr:{}\n{}", nevent, content_string);
    }

    Self::new(Kind::ChatMessage, content_string).tag(Tag::from_standardized_without_cell(
        TagStandard::Quote {
            event_id: reply_to.id,
            relay_url,
            public_key: Some(reply_to.pubkey),
        },
    ))
}

I don't think we need NostrParser here since all we need to do is check the start of a string. Using NostrParser would be very similar to this with the only difference being that instead of checking the start of a string we parse the whole string, get the first element, and check if its the correct nevent. So let me know what you think, maybe I have overlooked something.

@TheAwiteb
Copy link
Contributor

TheAwiteb commented Sep 7, 2025

I am new to Rust and Nostr so thanks for the help

Apologies for the delay in responding! You're absolutely welcome, happy to help.

I don't think we need NostrParser here since all we need to do is check the start of a string

We do need the NostrParser here because the Nip19Event you've constructed might not align with the existing one, as it could include additional details like the author, kind, or extra relays. It might also just be a note rather than a nevent. Using NostrParser simplifies the process, you can check if the event ID is present, and if not, you can add it while ensuring the provided relays are included.

@TheAwiteb
Copy link
Contributor

Also you can write this

content_string = format!("nostr:{}\n{}", nevent, content_string);

like this

content_string = format!("nostr:{nevent}\n{content_string}");

in your new change, it's more readable.

@SubatomicPlanets
Copy link
Contributor Author

Okay so something like this would work, right?

pub fn chat_message_reply<S>(content: S, reply_to: &Event, relay_url: Option<RelayUrl>) -> Self
where
    S: Into<String>,
{
    let mut content_string: String = content.into();

    let nevent = Nip19Event {
        event_id: reply_to.id,
        author: Some(reply_to.pubkey),
        kind: Some(reply_to.kind),
        relays: vec![],
    };
    let nevent_uri = nevent.to_nostr_uri().unwrap();

    let parsed = NostrParser::new().parse(&content_string).next();
    if !matches!(parsed, Some(Token::Nostr(nip21)) if nip21.event_id() == Some(reply_to.id)) {
        content_string = format!("{nevent_uri}\n{content_string}");
    }

    Self::new(Kind::ChatMessage, content_string).tag(Tag::from_standardized_without_cell(
        TagStandard::Quote {
            event_id: reply_to.id,
            relay_url,
            public_key: Some(reply_to.pubkey),
        },
    ))
}

It basically just parses the content, gets the first token, checks if it is an nevent and if it has the same event id as the reply_to event, and then it adds the nostr uri to the content based on those conditions. What should I do with the relays in Nip19Event though? Should I leave them empty, add only the optional relay_url (if it is not None), or add a third parameter (a vec of RelayUrls) for it? Thanks!

@TheAwiteb
Copy link
Contributor

TheAwiteb commented Sep 7, 2025

What should I do with the relays in Nip19Event though? Should I leave them empty, add only the optional relay_url (if it is not None), or add a third parameter (a vec of RelayUrls) for it? Thanks!

Instead of an empty Vec make it the provided relay if it's not None.

Also remove the author and kind from your nevent, they are useless.

And don't just check the first token, the nevent may be in the end of the content, instead of .next() use any function and check each token if it match the event id.

And don't unwrap the to_nostr_uri function, it can return an error if the relay url are very long :)

Thank you for your contribution.

@yukibtc
Copy link
Member

yukibtc commented Sep 8, 2025

Also I think you should parse the content using NostrParser and return an error if it doesn't contains the quoted event.

Or maybe add it if it doesn't contains it? the NIP-C7 shows it should be the first line. I'm not sure.

@yukibtc what do you think?

Yeah, I think make sense to automatically add it in the content if it doesn't exist.

@yukibtc
Copy link
Member

yukibtc commented Sep 8, 2025

@SubatomicPlanets, I would rewrite it like this, for better readability:

impl EventBuilder {
    // ...

    /// Chat message reply
    ///
    /// <https://github.com/nostr-protocol/nips/blob/master/C7.md>
    pub fn chat_message_reply<S>(
        content: S,
        reply_to: &Event,
        relay_url: Option<RelayUrl>,
    ) -> Result<Self, Error>
    where
        S: Into<String>,
    {
        let mut content: String = content.into();

        // Check if the content starts with a `nostr:` event uri
        if !starts_with_nostr_event_uri(&content, &reply_to.id)? {
            let nevent = Nip19Event {
                event_id: reply_to.id,
                author: Some(reply_to.pubkey),
                kind: Some(reply_to.kind),
                relays: relay_url.clone().into_iter().collect(),
            };
            let nevent_uri: String = nevent.to_nostr_uri()?;

            content = format!("{nevent_uri}\n{content}");
        }

        Ok(
            Self::new(Kind::ChatMessage, content).tag(Tag::from_standardized_without_cell(
                TagStandard::Quote {
                    event_id: reply_to.id,
                    relay_url,
                    public_key: Some(reply_to.pubkey),
                },
            )),
        )
    }
}

fn starts_with_nostr_event_uri(content: &str, event_id: &EventId) -> Result<bool, Error> {
    let token: Option<Token> = NostrParser::new().parse(content).next();

    if let Some(Token::Nostr(nip21)) = token {
        if let Some(id) = nip21.event_id() {
            return if &id == event_id {
                Ok(true)
            } else {
                Err(Error::) // Add an error to indicate that the event in the content is not right
            };
        }
    }

    Ok(false)
}

@SubatomicPlanets @TheAwiteb, what do you think?

@TheAwiteb
Copy link
Contributor

@SubatomicPlanets @TheAwiteb, what do you think

It's cool, but I don't like assuming that the quote are in the binging, it can be in the end.


my previous review:

And don't just check the first token, the nevent may be in the end of the content, instead of .next() use any function and check each token if it match the event id.

@yukibtc
Copy link
Member

yukibtc commented Sep 8, 2025

@SubatomicPlanets @TheAwiteb, what do you think

It's cool, but I don't like assuming that the quote are in the binging, it can be in the end.

my previous review:

And don't just check the first token, the nevent may be in the end of the content, instead of .next() use any function and check each token if it match the event id.

Ah, I've missed it.

Otherwise, what if we specify in doc that the content must be just the message without the nostr:nevent URI, which is automatically added using the reply_to event?

@TheAwiteb
Copy link
Contributor

Otherwise, what if we specify in doc that the content must be just the message without the nostr:nevent URI, which is automatically added using the reply_to event?

I think adding it if it's not exist is better, because maybe I want add a custom relays to it or the kind and the public key, or maybe not :).

We need to make it clear that if the content doesn't contains the reply_to event as a nostr-uri it will be added as the first line.

@TheAwiteb
Copy link
Contributor

@SubatomicPlanets If you need any help let me know

@yukibtc
Copy link
Member

yukibtc commented Sep 8, 2025

So, instead of the start_with_nostr_event_uri, something like this could be used:

fn has_nostr_event_uri(content: &str, event_id: &EventId) -> bool {
    const OPTS: NostrParserOptions = NostrParserOptions::disable_all().nostr_uris(true);
    
    let parser = NostrParser::new().parse(content).opts(OPTS);
    
    for token in parser.into_iter() {
        if let Token::Nostr(nip21) = token {
            if let Some(id) = nip21.event_id() {
                if &id == event_id {
                    return true;
                }
            }
        }
    }

    false
}

@TheAwiteb

@TheAwiteb
Copy link
Contributor

something like this could be used:

Yes, but this version is much simpler

fn has_nostr_event_uri(content: &str, event_id: &EventId) -> bool {
    const OPTS: NostrParserOptions = NostrParserOptions::disable_all().nostr_uris(true);

    NostrParser::new().parse(content).opts(OPTS).any(
        |token| matches!(token, Token::Nostr(uri) if uri.event_id().as_ref() == Some(event_id)),
    )
}

Copy link
Contributor

@TheAwiteb TheAwiteb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just this minor changes

/// Chat message reply
///
/// <https://github.com/nostr-protocol/nips/blob/master/C7.md>
#[inline]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yukibtc Do you think the #[inline] should be removed? Because it's not a small function anymore

Copy link
Contributor

@TheAwiteb TheAwiteb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiting for @yukibtc about removing the #[inline] from chat_message_reply function, it's not small anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants