From 25552ecb304ea598456d0163ea84d1e6b434657f Mon Sep 17 00:00:00 2001 From: Sunny Hashmi <6833405+sh4sh@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:39:16 -0500 Subject: [PATCH 1/4] Add XEP-0461 Message Replies support Co-authored-by: kousu --- xmpp.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/xmpp.go b/xmpp.go index 6998f732..e6b66b14 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1451,6 +1451,7 @@ func (c *Client) IsEncrypted() bool { // Chat is an incoming or outgoing XMPP chat message. type Chat struct { + ID string Remote string Type string Text string @@ -1465,7 +1466,9 @@ type Chat struct { // Only for incoming messages, ID for outgoing messages will be generated. OriginID string // Only for incoming messages, ID for outgoing messages will be generated. - StanzaID StanzaID + StanzaID StanzaID + // XEP-0461 + Reply Reply Roster Roster Other []string OtherElem []XMLElement @@ -1550,6 +1553,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { v.Delay.Stamp, ) chat := Chat{ + ID: v.ID, Remote: v.From, Type: v.Type, Text: v.Body, @@ -1561,6 +1565,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { Lang: v.Lang, OriginID: v.OriginID.ID, StanzaID: v.StanzaID, + Reply: v.Reply, Oob: v.Oob, } return chat, nil @@ -1852,12 +1857,24 @@ func (c *Client) Send(chat Chat) (n int, err error) { oobtext += `` } + var replytext string + if chat.Reply.ID != `` { + replytext = `` + } + chat.Text = validUTF8(chat.Text) - id := getUUID() + id := chat.ID + if id == "" { + id = getUUID() + } stanza := fmt.Sprintf("%s%s"+ - "%s%s\n", + "%s%s%s\n", xmlEscape(chat.Remote), xmlEscape(chat.Type), id, subtext, xmlEscape(chat.Text), - XMPPNS_SID_0, id, oobtext, thdtext) + replytext, XMPPNS_SID_0, id, oobtext, thdtext) if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", len(stanza), c.LimitMaxBytes) @@ -2166,6 +2183,13 @@ type StanzaID struct { By string `xml:"by,attr"` } +// XEP-0461 Message Replies +type Reply 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"` @@ -2184,6 +2208,9 @@ type clientMessage struct { OriginID originID `xml:"origin-id"` StanzaID StanzaID `xml:"stanza-id"` + // XEP-0461 + Reply Reply `xml:"reply"` + // Pubsub Event clientPubsubEvent `xml:"event"` From bce923c9b272e8a5095d5cab2b16699e0d4222ed Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 21:57:44 -0500 Subject: [PATCH 2/4] Drop log import --- xmpp.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/xmpp.go b/xmpp.go index e6b66b14..4f76e044 100644 --- a/xmpp.go +++ b/xmpp.go @@ -30,7 +30,6 @@ import ( "fmt" "hash" "io" - "log" "math/big" "net" "net/http" @@ -95,11 +94,7 @@ func getCookie() Cookie { func getUUID() string { // Use github.com/google/uuid as XEP-0359 requires an UUID according to // RFC 4122. - id, err := uuid.NewV7() - if err != nil { - log.Fatal(err) - } - return id.String() + return uuid.Must(uuid.NewV7()).String() } // Fast holds the XEP-0484 fast token, mechanism and expiry date From 033167d9d7e3384f65a879dde0bd261a2dbd3658 Mon Sep 17 00:00:00 2001 From: kousu Date: Thu, 5 Mar 2026 22:30:54 -0500 Subject: [PATCH 3/4] Make Reply nullable. is optional, so it should be optional in our datamodel too. --- xmpp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xmpp.go b/xmpp.go index 4f76e044..0300a98b 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1463,7 +1463,7 @@ type Chat struct { // Only for incoming messages, ID for outgoing messages will be generated. StanzaID StanzaID // XEP-0461 - Reply Reply + Reply *Reply Roster Roster Other []string OtherElem []XMLElement @@ -1853,7 +1853,7 @@ func (c *Client) Send(chat Chat) (n int, err error) { } var replytext string - if chat.Reply.ID != `` { + if chat.Reply != nil { replytext = ` Date: Thu, 5 Mar 2026 22:23:44 -0500 Subject: [PATCH 4/4] Support both xmpp.Chat.ID and xmpp.Chat.OriginID --- xmpp.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/xmpp.go b/xmpp.go index 0300a98b..cefbd53f 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1446,7 +1446,7 @@ func (c *Client) IsEncrypted() bool { // Chat is an incoming or outgoing XMPP chat message. type Chat struct { - ID string + ID string // if unset, will be generated on send Remote string Type string Text string @@ -1458,10 +1458,9 @@ type Chat struct { Ooburl string Oobdesc string Lang string - // Only for incoming messages, ID for outgoing messages will be generated. - OriginID string - // Only for incoming messages, ID for outgoing messages will be generated. - StanzaID StanzaID + // XEP-0359 + StanzaID StanzaID // only for incoming messages + OriginID string // if unset, will be generated on send // XEP-0461 Reply *Reply Roster Roster @@ -1862,14 +1861,18 @@ func (c *Client) Send(chat Chat) (n int, err error) { } chat.Text = validUTF8(chat.Text) - id := chat.ID - if id == "" { - id = getUUID() + + if chat.OriginID == "" { + chat.OriginID = getUUID() } + if chat.ID == "" { + chat.ID = chat.OriginID + } + stanza := fmt.Sprintf("%s%s"+ "%s%s%s\n", - xmlEscape(chat.Remote), xmlEscape(chat.Type), id, subtext, xmlEscape(chat.Text), - replytext, XMPPNS_SID_0, id, oobtext, thdtext) + xmlEscape(chat.Remote), xmlEscape(chat.Type), chat.ID, subtext, xmlEscape(chat.Text), + replytext, XMPPNS_SID_0, chat.OriginID, oobtext, thdtext) if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", len(stanza), c.LimitMaxBytes)