Skip to content

Commit

Permalink
Handle Whatsapp threading/replies (whatsapp) (42wim#1974)
Browse files Browse the repository at this point in the history
* 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
tulir/whatsmeow#88 (comment)
tulir/whatsmeow#148 (comment)

Based on commit 6afa93e from 42wim#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.
  • Loading branch information
yousefmansy1 committed Mar 11, 2023
1 parent 8dab942 commit fe7bbd3
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 24 deletions.
24 changes: 17 additions & 7 deletions bridge/whatsappmulti/handlers.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build whatsappmulti
// +build whatsappmulti

package bwhatsapp
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -251,15 +260,15 @@ 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,
Channel: msg.Info.Chat.String(),
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 {
Expand Down Expand Up @@ -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 {
Expand Down
63 changes: 63 additions & 0 deletions bridge/whatsappmulti/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
72 changes: 55 additions & 17 deletions bridge/whatsappmulti/whatsapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

0 comments on commit fe7bbd3

Please sign in to comment.