Handle Whatsapp threading/replies (whatsapp) (#1974)

* Handle Whatsapp threading/replies.
In this change we are updating whatsapp message IDs to include sender JID as this is necessary to reply to a message
https://github.com/tulir/whatsmeow/issues/88#issuecomment-1093195237
https://github.com/tulir/whatsmeow/discussions/148#discussioncomment-3094325

Based on commit 6afa93e537 from #1934
    Author: Iiro Laiho <iiro.laiho@iki.fi>

* Fix replies.
Sender JID can have a `:` inside of it, using `/` as a delimiter now.
Added messageID parser + struct.
messages sent with an attachment do not show replies
But at least common `sendMessage` will make repies from whatsapp to an attachement bridge across.

The new message ID format broke message deleting, so we change the messageID into the real id at the beginning of send.
We really do need the extra info for when we reply to a message though.

* Refactored message replies.
file/Image/audio/replies all work now.
This commit is contained in:
Joseph Mansy 2023-03-09 12:57:19 -08:00 committed by GitHub
parent d5980303e5
commit d00dcf3f58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 26 deletions

View File

@ -1,3 +1,4 @@
//go:build whatsappmulti
// +build whatsappmulti // +build whatsappmulti
package bwhatsapp package bwhatsapp
@ -89,6 +90,12 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
} }
} }
parentID := ""
if msg.GetExtendedTextMessage() != nil {
ci := msg.GetExtendedTextMessage().GetContextInfo()
parentID = getParentIdFromCtx(ci)
}
rmsg := config.Message{ rmsg := config.Message{
UserID: senderJID.String(), UserID: senderJID.String(),
Username: senderName, Username: senderName,
@ -97,8 +104,8 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string ID: getMessageIdFormat(senderJID, messageInfo.ID),
ID: messageInfo.ID, ParentID: parentID,
} }
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@ -130,7 +137,8 @@ func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
ID: msg.Info.ID, ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
} }
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@ -193,7 +201,8 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
ID: msg.Info.ID, ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
} }
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@ -251,7 +260,6 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
if senderJID == (types.JID{}) && ci.Participant != nil { if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
} }
rmsg := config.Message{ rmsg := config.Message{
UserID: senderJID.String(), UserID: senderJID.String(),
Username: senderName, Username: senderName,
@ -259,7 +267,8 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
ID: msg.Info.ID, ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
} }
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@ -316,7 +325,8 @@ func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
ID: msg.Info.ID, ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
} }
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {

View File

@ -7,7 +7,10 @@ import (
"fmt" "fmt"
"strings" "strings"
goproto "google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
@ -122,3 +125,63 @@ func (b *Bwhatsapp) getDevice() (*store.Device, error) {
return device, nil return device, nil
} }
func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) {
replyInfo, err := b.parseMessageID(parentID)
if err != nil {
return nil, err
}
sender := fmt.Sprintf("%s@%s", replyInfo.Sender.User, replyInfo.Sender.Server)
ctx := &proto.ContextInfo{
StanzaId: &replyInfo.MessageID,
Participant: &sender,
QuotedMessage: &proto.Message{Conversation: goproto.String("")},
}
return ctx, nil
}
func (b *Bwhatsapp) parseMessageID(id string) (*Replyable, error) {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
if id == "" {
return &Replyable{MessageID: id}, nil
}
replyInfo := strings.Split(id, "/")
if len(replyInfo) == 2 {
sender, err := types.ParseJID(replyInfo[0])
if err == nil {
return &Replyable{
MessageID: types.MessageID(replyInfo[1]),
Sender: sender,
}, nil
}
}
err := fmt.Errorf("MessageID does not match format of {senderJID}:{messageID} : \"%s\"", id)
return &Replyable{MessageID: id}, err
}
func getParentIdFromCtx(ci *proto.ContextInfo) string {
if ci != nil && ci.StanzaId != nil {
senderJid, err := types.ParseJID(*ci.Participant)
if err == nil {
return getMessageIdFormat(senderJid, *ci.StanzaId)
}
}
return ""
}
func getMessageIdFormat(jid types.JID, messageID string) string {
// we're crafting our own JID str as AD JID format messes with how stuff looks on a webclient
jidStr := fmt.Sprintf("%s@%s", jid.User, jid.Server)
return fmt.Sprintf("%s/%s", jidStr, messageID)
}

View File

@ -42,6 +42,11 @@ type Bwhatsapp struct {
userAvatars map[string]string userAvatars map[string]string
} }
type Replyable struct {
MessageID types.MessageID
Sender types.JID
}
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file // New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber) number := cfg.GetString(cfgNumber)
@ -222,6 +227,10 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
// Post document message // Post document message
var message proto.Message var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.DocumentMessage = &proto.DocumentMessage{ message.DocumentMessage = &proto.DocumentMessage{
Title: &fi.Name, Title: &fi.Name,
@ -233,6 +242,7 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
FileSha256: resp.FileSHA256, FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength), FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL, Url: &resp.URL,
ContextInfo: ctx,
} }
b.Log.Debugf("=> Sending %#v as a document", msg) b.Log.Debugf("=> Sending %#v as a document", msg)
@ -246,8 +256,6 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
// Post an image message from the bridge to WhatsApp // Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types // Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) { func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo) fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment caption := msg.Username + fi.Comment
@ -258,6 +266,10 @@ func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (strin
} }
var message proto.Message var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.ImageMessage = &proto.ImageMessage{ message.ImageMessage = &proto.ImageMessage{
Mimetype: &filetype, Mimetype: &filetype,
@ -267,20 +279,16 @@ func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256, FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength), FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL, Url: &resp.URL,
ContextInfo: ctx,
} }
b.Log.Debugf("=> Sending %#v as an image", msg) b.Log.Debugf("=> Sending %#v as an image", msg)
ID := whatsmeow.GenerateMessageID() return b.sendMessage(msg, &message)
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
return ID, err
} }
// Post a video message from the bridge to WhatsApp // Post a video message from the bridge to WhatsApp
func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) { func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo) fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment caption := msg.Username + fi.Comment
@ -291,6 +299,10 @@ func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (strin
} }
var message proto.Message var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.VideoMessage = &proto.VideoMessage{ message.VideoMessage = &proto.VideoMessage{
Mimetype: &filetype, Mimetype: &filetype,
@ -300,14 +312,12 @@ func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256, FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength), FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL, Url: &resp.URL,
ContextInfo: ctx,
} }
b.Log.Debugf("=> Sending %#v as a video", msg) b.Log.Debugf("=> Sending %#v as a video", msg)
ID := whatsmeow.GenerateMessageID() return b.sendMessage(msg, &message)
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
return ID, err
} }
// Post audio inline // Post audio inline
@ -322,6 +332,10 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
} }
var message proto.Message var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.AudioMessage = &proto.AudioMessage{ message.AudioMessage = &proto.AudioMessage{
Mimetype: &filetype, Mimetype: &filetype,
@ -330,12 +344,12 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256, FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength), FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL, Url: &resp.URL,
ContextInfo: ctx,
} }
b.Log.Debugf("=> Sending %#v as audio", msg) b.Log.Debugf("=> Sending %#v as audio", msg)
ID := whatsmeow.GenerateMessageID() ID, err := b.sendMessage(msg, &message)
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
var captionMessage proto.Message var captionMessage proto.Message
caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji
@ -351,6 +365,9 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
func (b *Bwhatsapp) Send(msg config.Message) (string, error) { func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel) groupJID, _ := types.ParseJID(msg.Channel)
extendedMsgID, _ := b.parseMessageID(msg.ID)
msg.ID = extendedMsgID.MessageID
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
// Delete message // Delete message
@ -400,14 +417,35 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
} }
} }
var message proto.Message
text := msg.Username + msg.Text text := msg.Username + msg.Text
var message proto.Message // If we have a parent ID send an extended message
if msg.ParentID != "" {
replyContext, err := b.getNewReplyContext(msg.ParentID)
if err == nil {
message = proto.Message{
ExtendedTextMessage: &proto.ExtendedTextMessage{
Text: &text,
ContextInfo: replyContext,
},
}
return b.sendMessage(msg, &message)
}
}
message.Conversation = &text message.Conversation = &text
ID := whatsmeow.GenerateMessageID() return b.sendMessage(msg, &message)
_, err := b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID}) }
return ID, err func (b *Bwhatsapp) sendMessage(rmsg config.Message, message *proto.Message) (string, error) {
groupJID, _ := types.ParseJID(rmsg.Channel)
ID := whatsmeow.GenerateMessageID()
_, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID})
return getMessageIdFormat(*b.wc.Store.ID, ID), err
} }