Skip to content

Add support for XEP-0461: Message Replies#226

Open
sh4sh wants to merge 4 commits intoxmppo:masterfrom
sh4sh:xmpp-reply
Open

Add support for XEP-0461: Message Replies#226
sh4sh wants to merge 4 commits intoxmppo:masterfrom
sh4sh:xmpp-reply

Conversation

@sh4sh
Copy link
Copy Markdown

@sh4sh sh4sh commented Feb 16, 2026

XEP-0461: Message Replies
We now generate the reply object and correctly attribute author.

I didn't implement 3.1 Compatibility fallback, we're still compliant without but I can look into it soon.

kousu added a commit to sh4sh/matterbridge that referenced this pull request Feb 18, 2026
- xmppo/go-xmpp#226

Drop this commit if/when that is merged.
kousu added a commit to sh4sh/matterbridge that referenced this pull request Feb 18, 2026
Contains:

- xmppo/go-xmpp#226

Drop this commit if/when those are merged upstream.
kousu added a commit to sh4sh/matterbridge that referenced this pull request Feb 18, 2026
Contains:

- xmppo/go-xmpp#226

Drop this commit if/when those are merged upstream.
@selfhoster1312
Copy link
Copy Markdown
Contributor

Hello! Thanks for the contribution!

The complexity is groupchat and direct chat use different logic. I'm curious about @mdosch's opinion but personally i prefer more typing. In your PR, Reply can contain any form of ID which is error prone.

I'd personally either enjoy different types (does not fit the current API design, or golang patterns very well), but i think helper methods can be good:

func (m *xmpp.Chat) withMucReply(to StanzaID) {
...
}

func (m *xmpp.Chat) withDirectMessageReply(to originID) {
...
}

func (m *xmpp.Chat) withDirectMessageReplyFallback(to string) {
...
}

This would avoid clients using the wrong ID unintentionally. It would require to make the OriginID public which i think is required anyway to enable direct message replies.

About the compatibility fallback, it looks more complex to implement (checking indices within the body string), but i think it's definitely worth it at least on the receiving side. I'm not sure what's a good API design for that, but we don't want a go-xmpp client receiving a reply to consider the entire quoted fallback message, i believe. Opinions?

@sh4sh
Copy link
Copy Markdown
Author

sh4sh commented Feb 19, 2026

Oh, thank you! I was admittedly not thinking of the direct message case here. Adding helper methods makes sense to me, and they have the side benefit of improving documentation.

For fallback replies, this is what the fallback object would look like per XEP-0428:

  <fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
    <body start='0' end='33' />
  </fallback>

so for incoming messages, I think we just need:

  1. if fallback.for == "urn:xmpp:reply:0", then:
  2. slice out the > formatting from chat.Text using the body start/end offsets

edit: oh. I forgot about Go string slicing. perhaps we use runes here?

Does that make sense? Are there any complexities I'm not considering?

@kousu
Copy link
Copy Markdown

kousu commented Feb 19, 2026

we don't want a go-xmpp client receiving a reply to consider the entire quoted fallback message, i believe. Opinions?

Not to pile on more work for the sake of more work for @sh4sh but go-xmpp should not strip the quote fully. It has to be up to the client to decide what to do, because only the client knows. I think go-xmpp should parse <fallback> and separate the quote to chat.ReplyFallback from the message in chat.Text.

From the XEP:

The receiving client SHOULD NOT display the compatibility fallback if it renders the referenced message alongside the reply in some way. However, if the receiving client does not display the referenced message, for example because it doesn't know the referenced id, it MAY display the compatibility fallback instead.

Consider in the context of matterbridge as a client: some protocols don't support replies, so if go-xmpp strips the incoming quote before matterbridge sees the message, people on those protocols will be confused.

@sh4sh
Copy link
Copy Markdown
Author

sh4sh commented Feb 19, 2026

I edited my message to be clearer that my solution is for parsing replies on incoming messages. Do we need logic to ensure the quote is not stripped if the client doesn't use our reply structure? Or maybe we can re-add it?

Here is a full stanza with reply and reply fallback:

<message to='anna@example.com' id='message-id3' type='chat'>
  <body>
    > Anna wrote:
    > We should bake a cake
    Great idea!
  </body>
  <reply to='anna@example.com/laptop' id='message-id1' xmlns='urn:xmpp:reply:0' />
  <fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
    <body start="0" end="38" />
  </fallback>
</message>

I agree that for outgoing messages, it would be confusing to remove the fallback formatting while still including the quote in message body. Is there a case where go-xmpp would be formatting quotes into the outgoing message body, or should that be handled by the client?

And do we want to store the quote somewhere, so the client/protocol receiving our messages can access it? Chat.ReplyFallback?

@selfhoster1312
Copy link
Copy Markdown
Contributor

These are all very good points, thank you! I'm not very familiar with unicode representations (or golang), but it looks like a rune is indeed a unicode codepoint as expected by XEP-0246. However, that spec also says when the offset (start or end here) ends up in the middle of a grapheme, we should either:

  • Split the grapheme cluster into multiple graphemes. In most cases, this is closest to the intended behavior. Many font display engines will do this automatically as needed.
  • When the offset defines the end of a region, include the full grapheme cluster in the region. Otherwise, take the offset as if it pointed to the beginning of the grapheme cluster.

The easiest would of course to be not handle any of that and leave it up to the client. However, since that's very specific i think a correct implementation in go-xmpp is actually better. I also believe we should make it easy for clients supporting message replies to access the stripped body (without the fallback quote), but in order to avoid a breaking change for clients not implementing it, it should not be the default, but maybe a helper method. Maybe something like:

func (m *xmpp.Chat) BodyWithReply() (string, string, err)

But it's a good point that the withMucReply i proposed earlier does not address the fallback, because go-xmpp keeps minimal internal state and therefore cannot fill in the blank. Also, the method would not produce a different class instance but rather modify the existing one, so Add is semantically better than With. So the signature may be more like:

func (m *xmpp.Chat) AddMucReply(to StanzaId, fallback string)

(where fallback would contain the original message text, which could then be formatted into the actual body by go-xmpp)

Of course we're speculating here but the maintainers of go-xmpp probably have feedback and opinions about how to best handle those situations. Let's not put any pressure on them. I'll be happy to test/review your matterbridge PR before this pull-request is merged here :-)

kousu added a commit to sh4sh/matterbridge that referenced this pull request Feb 21, 2026
- xmppo/go-xmpp#226

Drop this commit if/when that is merged.
kousu added a commit to sh4sh/matterbridge that referenced this pull request Feb 22, 2026
- xmppo/go-xmpp#226

Drop this commit if/when that is merged.
var replytext string
if chat.Reply.ID != `` {
replytext = `<reply id='` + xmlEscape(chat.Reply.ID) + `'`
if chat.Reply.To != `` {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The spec says there SHOULD be a to field. I think we don't have a reason to escape this and we should produce an error when id or to is an empty string. Do you agree?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

oh yeah, nice! Makes sense to me, will fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The incoming side has to allow for missing tos because SHOULD means optional; I don't think it hurts anything to be symmetric on the sending side.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think it hurts anything to be symmetric on the sending side.

It doesn't hurt. But a SHOULD has a clear meaning in RFCs which means unless you have a very strong reason not to do it, you have to do it (a MUST accepts no exception). It's not a MAY so my opinion is we should not make it optional on the sending side.

In the end the maintainers of the library will decide whatever fits more the library as a whole. That was just my personal opinion :)

}

chat.Text = validUTF8(chat.Text)
id := getUUID()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason why you removed this part where go-xmpp generates a new ID automatically and ignores any previously set ID on the message? I think both are reasonable behavior, but if you have an argument for this, it would probably be better in a separate PR (personal opinion).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is that because in order to handle replies properly the client should keep track of sent origin-ids? And since the Send method for some reason returns the number of bytes written instead of the generated ID, we need to handle that outside of the function signature to avoid a breaking change?

Copy link
Copy Markdown
Author

@sh4sh sh4sh Feb 23, 2026

Choose a reason for hiding this comment

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

Yes, exactly. The client needs an ID that is referenced later on reply, when it turns into origin-id id. (see change in matterbridge#134 where it gets generated)

...it would be nice to just use the UUID that go-xmpp generates, but yeah it's not already returned in Send() so it felt out of reach.

This seemed like the cleanest way. open to suggestions!

@selfhoster1312
Copy link
Copy Markdown
Contributor

Should we advertise support for XEP-0461 in disco info? By default, we only handle the fallback, so by default we should not advertise the feature. However, i don't think go-xmpp handles features in disco queries yet? Did i miss something?

Co-authored-by: kousu <nick@kousu.ca>
kousu added 3 commits March 5, 2026 22:13
<reply> is optional, so it should be optional in our datamodel too.
kousu added a commit to sh4sh/matterbridge that referenced this pull request Mar 6, 2026
kousu added a commit to sh4sh/matterbridge that referenced this pull request Mar 6, 2026
@kousu
Copy link
Copy Markdown

kousu commented Mar 6, 2026

And do we want to store the quote somewhere, so the client/protocol receiving our messages can access it? Chat.ReplyFallback?

I've implemented send/receive in 1 2, specifically matterbridge-org/matterbridge@f8cb9ab and 9e62a00.

I split the quote out to Chat.Reply.Quote, alongside Chat.Reply.ID and Chat.Reply.To.

I haven't pushed it to xmpp-reply yet because I don't want to derail figuring out which ID to use. But here's how you can test it all bundled together:

cd $(mktemp -d)
git clone --depth 1 -b xmpp-reply-fallback https://github.com/kousu/matterbridge
cd matterbridge
go build
./matterbridge -version

Implementing correctly turned out to be pretty wack 😅 ; I had to pull in a library for handling interval arithmetic. It's one file and MIT licensed so we could vendor it if bloating the dependency tree is a worry. I don't think it's avoidable though, the way is defined implies doing interval subtraction to determine the final message text. I'm proud of it though, I think it's a complete basis for adding other fallbacks.

(it's worth having a solid basis because Cheogram as least "uses it for many things")

kousu added a commit to sh4sh/matterbridge that referenced this pull request Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants