summaryrefslogtreecommitdiffstats
path: root/bridge/whatsappmulti
diff options
context:
space:
mode:
authorJoseph Mansy <36427684+yousefmansy1@users.noreply.github.com>2023-03-09 12:57:19 -0800
committerGitHub <noreply@github.com>2023-03-09 21:57:19 +0100
commitd00dcf3f58ba8f6a0919e8bdadfe9ba89864ed8d (patch)
treef3261b9d1963d5899fd267f414cd6867d9c44a78 /bridge/whatsappmulti
parentd5980303e5344b5c7c0804f712c4c5365fd6c929 (diff)
downloadmatterbridge-msglm-d00dcf3f58ba8f6a0919e8bdadfe9ba89864ed8d.tar.gz
matterbridge-msglm-d00dcf3f58ba8f6a0919e8bdadfe9ba89864ed8d.tar.bz2
matterbridge-msglm-d00dcf3f58ba8f6a0919e8bdadfe9ba89864ed8d.zip
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 6afa93e537c53b371db37f35a3546ff0fb669416 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.
Diffstat (limited to 'bridge/whatsappmulti')
-rw-r--r--bridge/whatsappmulti/handlers.go24
-rw-r--r--bridge/whatsappmulti/helpers.go63
-rw-r--r--bridge/whatsappmulti/whatsapp.go72
3 files changed, 135 insertions, 24 deletions
diff --git a/bridge/whatsappmulti/handlers.go b/bridge/whatsappmulti/handlers.go
index fd75be09..b8e78572 100644
--- a/bridge/whatsappmulti/handlers.go
+++ b/bridge/whatsappmulti/handlers.go
@@ -1,3 +1,4 @@
+//go:build whatsappmulti
// +build whatsappmulti
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{
UserID: senderJID.String(),
Username: senderName,
@@ -97,8 +104,8 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
- // ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
- ID: messageInfo.ID,
+ ID: getMessageIdFormat(senderJID, messageInfo.ID),
+ ParentID: parentID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@@ -130,7 +137,8 @@ func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
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 {
@@ -193,7 +201,8 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
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 {
@@ -251,7 +260,6 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
-
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
@@ -259,7 +267,8 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
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 {
@@ -316,7 +325,8 @@ func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
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 {
diff --git a/bridge/whatsappmulti/helpers.go b/bridge/whatsappmulti/helpers.go
index 75d9f5fd..963eafa1 100644
--- a/bridge/whatsappmulti/helpers.go
+++ b/bridge/whatsappmulti/helpers.go
@@ -7,7 +7,10 @@ import (
"fmt"
"strings"
+ goproto "google.golang.org/protobuf/proto"
+
"go.mau.fi/whatsmeow"
+ "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
@@ -122,3 +125,63 @@ func (b *Bwhatsapp) getDevice() (*store.Device, error) {
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)
+}
diff --git a/bridge/whatsappmulti/whatsapp.go b/bridge/whatsappmulti/whatsapp.go
index 76e4ae46..6798d827 100644
--- a/bridge/whatsappmulti/whatsapp.go
+++ b/bridge/whatsappmulti/whatsapp.go
@@ -42,6 +42,11 @@ type Bwhatsapp struct {
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
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
@@ -222,6 +227,10 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
// Post document message
var message proto.Message
+ var ctx *proto.ContextInfo
+ if msg.ParentID != "" {
+ ctx, _ = b.getNewReplyContext(msg.ParentID)
+ }
message.DocumentMessage = &proto.DocumentMessage{
Title: &fi.Name,
@@ -233,6 +242,7 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
+ ContextInfo: ctx,
}
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
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
- groupJID, _ := types.ParseJID(msg.Channel)
-
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
@@ -258,6 +266,10 @@ func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (strin
}
var message proto.Message
+ var ctx *proto.ContextInfo
+ if msg.ParentID != "" {
+ ctx, _ = b.getNewReplyContext(msg.ParentID)
+ }
message.ImageMessage = &proto.ImageMessage{
Mimetype: &filetype,
@@ -267,20 +279,16 @@ func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
+ ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as an image", msg)
- ID := whatsmeow.GenerateMessageID()
- _, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
-
- return ID, err
+ return b.sendMessage(msg, &message)
}
// Post a video message from the bridge to WhatsApp
func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) {
- groupJID, _ := types.ParseJID(msg.Channel)
-
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
@@ -291,6 +299,10 @@ func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (strin
}
var message proto.Message
+ var ctx *proto.ContextInfo
+ if msg.ParentID != "" {
+ ctx, _ = b.getNewReplyContext(msg.ParentID)
+ }
message.VideoMessage = &proto.VideoMessage{
Mimetype: &filetype,
@@ -300,14 +312,12 @@ func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
+ ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as a video", msg)
- ID := whatsmeow.GenerateMessageID()
- _, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
-
- return ID, err
+ return b.sendMessage(msg, &message)
}
// Post audio inline
@@ -322,6 +332,10 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
}
var message proto.Message
+ var ctx *proto.ContextInfo
+ if msg.ParentID != "" {
+ ctx, _ = b.getNewReplyContext(msg.ParentID)
+ }
message.AudioMessage = &proto.AudioMessage{
Mimetype: &filetype,
@@ -330,12 +344,12 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
+ ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as audio", msg)
- ID := whatsmeow.GenerateMessageID()
- _, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
+ ID, err := b.sendMessage(msg, &message)
var captionMessage proto.Message
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) {
groupJID, _ := types.ParseJID(msg.Channel)
+ extendedMsgID, _ := b.parseMessageID(msg.ID)
+ msg.ID = extendedMsgID.MessageID
+
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
@@ -400,14 +417,35 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
}
}
+ var message proto.Message
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
+ return b.sendMessage(msg, &message)
+}
+
+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.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
- return ID, err
+ _, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID})
+
+ return getMessageIdFormat(*b.wc.Store.ID, ID), err
}