From b5fdf413cd3925562a04acd29653a5f9d8b5cd56 Mon Sep 17 00:00:00 2001 From: kousu Date: Mon, 9 Feb 2026 18:30:11 -0500 Subject: [PATCH 1/8] Add XEP-0461 Message Replies support Add ReplyTo field to Chat struct and include element in Send() when set. This enables XMPP clients to send messages that reference a parent message per the XEP-0461 specification. Co-Authored-By: Claude Opus 4.5 --- xmpp.go | 10 ++++++-- xmpp_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/xmpp.go b/xmpp.go index 30a7ff74..5c5a6ec8 100644 --- a/xmpp.go +++ b/xmpp.go @@ -609,6 +609,7 @@ type Chat struct { Oobdesc string ID string ReplaceID string + ReplyTo string // XEP-0461: Reply to message ID Roster Roster Other []string OtherElem []XMLElement @@ -867,7 +868,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { // Send sends the message wrapped inside an XMPP message stanza body. func (c *Client) Send(chat Chat) (n int, err error) { - var subtext, thdtext, oobtext, msgidtext, msgcorrecttext string + var subtext, thdtext, oobtext, msgidtext, msgcorrecttext, replytext string if chat.Subject != `` { subtext = `` + xmlEscape(chat.Subject) + `` } @@ -892,7 +893,12 @@ func (c *Client) Send(chat Chat) (n int, err error) { msgcorrecttext = `` } - stanza := "" + subtext + "%s" + msgcorrecttext + oobtext + thdtext + "" + // XEP-0461: Message Replies + if chat.ReplyTo != `` { + replytext = `` + } + + stanza := "" + subtext + "%s" + msgcorrecttext + replytext + oobtext + thdtext + "" return fmt.Fprintf(c.conn, stanza, xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) } diff --git a/xmpp_test.go b/xmpp_test.go index 2966d98e..68c224fe 100644 --- a/xmpp_test.go +++ b/xmpp_test.go @@ -127,7 +127,7 @@ func TestEmptyPubsub(t *testing.T) { c.conn = tConnect(emptyPubSub) c.p = xml.NewDecoder(c.conn) m, err := c.Recv() - + switch m.(type) { case AvatarData: if err == nil { @@ -138,3 +138,73 @@ func TestEmptyPubsub(t *testing.T) { t.Errorf("Expected a return value of AvatarData") } } + +func TestSendReply(t *testing.T) { + var c Client + buf := &bytes.Buffer{} + c.conn = &testConn{buf} + + chat := Chat{ + Remote: "room@conference.example.com", + Type: "groupchat", + Text: "This is a reply", + ID: "msg-123", + ReplyTo: "original-msg-456", + } + + _, err := c.Send(chat) + if err != nil { + t.Fatalf("Send() returned error: %v", err) + } + + output := buf.String() + + // Check that the reply element is present with correct namespace + expectedReply := `` + if !strings.Contains(output, expectedReply) { + t.Errorf("Send() output missing XEP-0461 reply element.\nGot: %s\nExpected to contain: %s", output, expectedReply) + } + + // Check that the body is present + if !strings.Contains(output, "This is a reply") { + t.Errorf("Send() output missing body element.\nGot: %s", output) + } + + // Check message attributes + if !strings.Contains(output, "to='room@conference.example.com'") { + t.Errorf("Send() output missing 'to' attribute.\nGot: %s", output) + } + if !strings.Contains(output, "type='groupchat'") { + t.Errorf("Send() output missing 'type' attribute.\nGot: %s", output) + } +} + +func TestSendWithoutReply(t *testing.T) { + var c Client + buf := &bytes.Buffer{} + c.conn = &testConn{buf} + + chat := Chat{ + Remote: "room@conference.example.com", + Type: "groupchat", + Text: "Regular message", + ID: "msg-789", + } + + _, err := c.Send(chat) + if err != nil { + t.Fatalf("Send() returned error: %v", err) + } + + output := buf.String() + + // Check that no reply element is present when ReplyTo is empty + if strings.Contains(output, "Regular message") { + t.Errorf("Send() output missing body element.\nGot: %s", output) + } +} From 30963c22ebcdf98ea5b5764d5933390e7e9ce6ee Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:27:58 -0500 Subject: [PATCH 2/8] added ReplyID to Chat struct to go with the ReplyTo, and added clientReply struct to build clientMessage's reply object since ID and To are nested. --- xmpp.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/xmpp.go b/xmpp.go index 5c5a6ec8..e40ab5aa 100644 --- a/xmpp.go +++ b/xmpp.go @@ -609,7 +609,8 @@ type Chat struct { Oobdesc string ID string ReplaceID string - ReplyTo string // XEP-0461: Reply to message ID + ReplyID string // XEP-0461: id of the message being replied to (use stanza-id for groupchat) + ReplyTo string // XEP-0461: JID of the author of the message being replied to Roster Roster Other []string OtherElem []XMLElement @@ -1020,6 +1021,12 @@ type clientMessageCorrect struct { ID string `xml:"id,attr"` } +type clientReply struct { + XMLName xml.Name `xml:"urn:xmpp:reply:0 reply"` + ID string `xml:"id,attr"` + To string `xml:"to,attr"` +} + // RFC 3921 B.1 jabber:client type clientMessage struct { XMLName xml.Name `xml:"jabber:client message"` @@ -1033,6 +1040,7 @@ type clientMessage struct { Body string `xml:"body"` Thread string `xml:"thread"` ReplaceID clientMessageCorrect + Reply clientReply // Pubsub Event clientPubsubEvent `xml:"event"` From 8517b2059f6d0bcae0afd7e68552eb3b1d78d577 Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:18:37 -0500 Subject: [PATCH 3/8] assigned ReplyID and ReplyTo in Chat, built replytext and added to stanza --- xmpp.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/xmpp.go b/xmpp.go index e40ab5aa..8788e8c6 100644 --- a/xmpp.go +++ b/xmpp.go @@ -686,6 +686,8 @@ func (c *Client) Recv() (stanza interface{}, err error) { Thread: v.Thread, ID: v.ID, ReplaceID: v.ReplaceID.ID, + ReplyID: v.Reply.ID, + ReplyTo: v.Reply.To, Other: v.OtherStrings(), OtherElem: v.Other, Stamp: stamp, @@ -869,7 +871,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { // Send sends the message wrapped inside an XMPP message stanza body. func (c *Client) Send(chat Chat) (n int, err error) { - var subtext, thdtext, oobtext, msgidtext, msgcorrecttext, replytext string + var subtext, thdtext, oobtext, msgidtext, msgcorrecttext string if chat.Subject != `` { subtext = `` + xmlEscape(chat.Subject) + `` } @@ -894,12 +896,16 @@ func (c *Client) Send(chat Chat) (n int, err error) { msgcorrecttext = `` } - // XEP-0461: Message Replies - if chat.ReplyTo != `` { - replytext = `` + var replytext string + if chat.ReplyID != `` { + replytext = `` } - stanza := "" + subtext + "%s" + msgcorrecttext + replytext + oobtext + thdtext + "" + stanza := "" + subtext + "%s" + replytext + msgcorrecttext + oobtext + thdtext + "" return fmt.Fprintf(c.conn, stanza, xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) } From 5a077e77e1e4a66cb0ab52406da923103571deeb Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:00:45 -0500 Subject: [PATCH 4/8] add stanza-id to Chat, fix tests --- xmpp.go | 11 +++++++++++ xmpp_test.go | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/xmpp.go b/xmpp.go index 8788e8c6..e30ebcf7 100644 --- a/xmpp.go +++ b/xmpp.go @@ -611,6 +611,8 @@ type Chat struct { ReplaceID string ReplyID string // XEP-0461: id of the message being replied to (use stanza-id for groupchat) ReplyTo string // XEP-0461: JID of the author of the message being replied to + StanzaID string // XEP-0359: refers to stanza-id but named Stanza for brevity + StanzaBy string // XEP-0359: refers to stanza-id but named Stanza for brevity Roster Roster Other []string OtherElem []XMLElement @@ -688,6 +690,8 @@ func (c *Client) Recv() (stanza interface{}, err error) { ReplaceID: v.ReplaceID.ID, ReplyID: v.Reply.ID, ReplyTo: v.Reply.To, + StanzaID: v.StanzaID.ID, + StanzaBy: v.StanzaID.By, Other: v.OtherStrings(), OtherElem: v.Other, Stamp: stamp, @@ -1033,6 +1037,12 @@ type clientReply struct { To string `xml:"to,attr"` } +type clientStanzaID struct { + XMLName xml.Name `xml:"urn:xmpp:stanza-id:0 stanza-id"` + ID string `xml:"id,attr"` + By string `xml:"by,attr"` +} + // RFC 3921 B.1 jabber:client type clientMessage struct { XMLName xml.Name `xml:"jabber:client message"` @@ -1047,6 +1057,7 @@ type clientMessage struct { Thread string `xml:"thread"` ReplaceID clientMessageCorrect Reply clientReply + StanzaID clientStanzaID // Pubsub Event clientPubsubEvent `xml:"event"` diff --git a/xmpp_test.go b/xmpp_test.go index 68c224fe..504b830f 100644 --- a/xmpp_test.go +++ b/xmpp_test.go @@ -56,7 +56,7 @@ func (*testConn) SetWriteDeadline(time.Time) error { } var text = strings.TrimSpace(` - + {"random": "<text>"} @@ -150,6 +150,7 @@ func TestSendReply(t *testing.T) { Text: "This is a reply", ID: "msg-123", ReplyTo: "original-msg-456", + ReplyID: "reply-id-789", } _, err := c.Send(chat) @@ -160,7 +161,7 @@ func TestSendReply(t *testing.T) { output := buf.String() // Check that the reply element is present with correct namespace - expectedReply := `` + expectedReply := `` if !strings.Contains(output, expectedReply) { t.Errorf("Send() output missing XEP-0461 reply element.\nGot: %s\nExpected to contain: %s", output, expectedReply) } From 5644e817e16ecfa8b9bd1e4367e22dff569a1b84 Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:47:20 -0500 Subject: [PATCH 5/8] fix stanza-id xmlname --- xmpp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmpp.go b/xmpp.go index e30ebcf7..ef72ad38 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1038,7 +1038,7 @@ type clientReply struct { } type clientStanzaID struct { - XMLName xml.Name `xml:"urn:xmpp:stanza-id:0 stanza-id"` + XMLName xml.Name `xml:"urn:xmpp:sid:0 stanza-id"` ID string `xml:"id,attr"` By string `xml:"by,attr"` } From a8b69d520747eb0169ab73a17372fb8410711491 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 10 Feb 2026 22:41:24 -0500 Subject: [PATCH 6/8] Run gofmt --- xmpp.go | 2 +- xmpp_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/xmpp.go b/xmpp.go index ef72ad38..eeb3503c 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1057,7 +1057,7 @@ type clientMessage struct { Thread string `xml:"thread"` ReplaceID clientMessageCorrect Reply clientReply - StanzaID clientStanzaID + StanzaID clientStanzaID // Pubsub Event clientPubsubEvent `xml:"event"` diff --git a/xmpp_test.go b/xmpp_test.go index 504b830f..b5f7420a 100644 --- a/xmpp_test.go +++ b/xmpp_test.go @@ -122,6 +122,7 @@ var emptyPubSub = strings.TrimSpace(` `) + func TestEmptyPubsub(t *testing.T) { var c Client c.conn = tConnect(emptyPubSub) From 32211a61bb14b6bb274fcd2d2c56c6ec91c5373b Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:40:37 -0500 Subject: [PATCH 7/8] remove unused Chat struct members --- xmpp.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/xmpp.go b/xmpp.go index eeb3503c..a5757b58 100644 --- a/xmpp.go +++ b/xmpp.go @@ -609,10 +609,7 @@ type Chat struct { Oobdesc string ID string ReplaceID string - ReplyID string // XEP-0461: id of the message being replied to (use stanza-id for groupchat) - ReplyTo string // XEP-0461: JID of the author of the message being replied to - StanzaID string // XEP-0359: refers to stanza-id but named Stanza for brevity - StanzaBy string // XEP-0359: refers to stanza-id but named Stanza for brevity + Reply string // XEP-0461: id of the message being replied to (use stanza-id for groupchat) Roster Roster Other []string OtherElem []XMLElement @@ -688,10 +685,6 @@ func (c *Client) Recv() (stanza interface{}, err error) { Thread: v.Thread, ID: v.ID, ReplaceID: v.ReplaceID.ID, - ReplyID: v.Reply.ID, - ReplyTo: v.Reply.To, - StanzaID: v.StanzaID.ID, - StanzaBy: v.StanzaID.By, Other: v.OtherStrings(), OtherElem: v.Other, Stamp: stamp, From 1624760a2da81605dcaecdf860a93037a4f0cadb Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:06:21 -0500 Subject: [PATCH 8/8] Revert "remove unused Chat struct members" This reverts commit 32211a61bb14b6bb274fcd2d2c56c6ec91c5373b. oops, we needed those --- xmpp.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xmpp.go b/xmpp.go index a5757b58..eeb3503c 100644 --- a/xmpp.go +++ b/xmpp.go @@ -609,7 +609,10 @@ type Chat struct { Oobdesc string ID string ReplaceID string - Reply string // XEP-0461: id of the message being replied to (use stanza-id for groupchat) + ReplyID string // XEP-0461: id of the message being replied to (use stanza-id for groupchat) + ReplyTo string // XEP-0461: JID of the author of the message being replied to + StanzaID string // XEP-0359: refers to stanza-id but named Stanza for brevity + StanzaBy string // XEP-0359: refers to stanza-id but named Stanza for brevity Roster Roster Other []string OtherElem []XMLElement @@ -685,6 +688,10 @@ func (c *Client) Recv() (stanza interface{}, err error) { Thread: v.Thread, ID: v.ID, ReplaceID: v.ReplaceID.ID, + ReplyID: v.Reply.ID, + ReplyTo: v.Reply.To, + StanzaID: v.StanzaID.ID, + StanzaBy: v.StanzaID.By, Other: v.OtherStrings(), OtherElem: v.Other, Stamp: stamp,