1
0
Fork 0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-05-15 10:16:52 -04:00
mautrix-whatsapp/pkg/msgconv/wa-business.go

330 lines
13 KiB
Go
Raw Permalink Normal View History

2024-09-25 16:01:35 +03:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package msgconv
import (
"context"
"fmt"
"strings"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
)
func (mc *MessageConverter) convertTemplateMessage(ctx context.Context, info *types.MessageInfo, tplMsg *waE2E.TemplateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
tpl := tplMsg.GetHydratedTemplate()
if tpl == nil {
tpl = tplMsg.GetHydratedFourRowTemplate()
}
if tpl == nil {
if interactiveMsg := tplMsg.GetInteractiveMessageTemplate(); interactiveMsg != nil {
return mc.convertInteractiveMessage(ctx, info, interactiveMsg)
}
// TODO there are a few other types too
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: "Unsupported business message (template)",
MsgType: event.MsgText,
},
}, tplMsg.GetContextInfo()
2024-09-25 16:01:35 +03:00
}
content := tpl.GetHydratedContentText()
2024-09-25 16:01:35 +03:00
if buttons := tpl.GetHydratedButtons(); len(buttons) > 0 {
addButtonText := false
descriptions := make([]string, len(buttons))
for i, rawButton := range buttons {
switch button := rawButton.GetHydratedButton().(type) {
case *waE2E.HydratedTemplateButton_QuickReplyButton:
descriptions[i] = fmt.Sprintf("<%s>", button.QuickReplyButton.GetDisplayText())
addButtonText = true
case *waE2E.HydratedTemplateButton_UrlButton:
descriptions[i] = fmt.Sprintf("[%s](%s)", button.UrlButton.GetDisplayText(), button.UrlButton.GetURL())
case *waE2E.HydratedTemplateButton_CallButton:
descriptions[i] = fmt.Sprintf("[%s](tel:%s)", button.CallButton.GetDisplayText(), button.CallButton.GetPhoneNumber())
}
}
description := strings.Join(descriptions, " - ")
if addButtonText {
description += "\nUse the WhatsApp app to click buttons"
}
content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, description))
2024-09-25 16:01:35 +03:00
}
if footer := tpl.GetHydratedFooterText(); footer != "" {
content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, footer))
2024-09-25 16:01:35 +03:00
}
var convertedTitle *bridgev2.ConvertedMessagePart
switch title := tpl.GetTitle().(type) {
case *waE2E.TemplateMessage_HydratedFourRowTemplate_DocumentMessage:
2024-11-06 13:14:12 +01:00
convertedTitle, _ = mc.convertMediaMessage(ctx, title.DocumentMessage, "file attachment", info, false, nil)
2024-09-25 16:01:35 +03:00
case *waE2E.TemplateMessage_HydratedFourRowTemplate_ImageMessage:
2024-11-06 13:14:12 +01:00
convertedTitle, _ = mc.convertMediaMessage(ctx, title.ImageMessage, "photo", info, false, nil)
2024-09-25 16:01:35 +03:00
case *waE2E.TemplateMessage_HydratedFourRowTemplate_VideoMessage:
2024-11-06 13:14:12 +01:00
convertedTitle, _ = mc.convertMediaMessage(ctx, title.VideoMessage, "video attachment", info, false, nil)
2024-09-25 16:01:35 +03:00
case *waE2E.TemplateMessage_HydratedFourRowTemplate_LocationMessage:
content = fmt.Sprintf("Unsupported location message\n\n%s", content)
2024-09-25 16:01:35 +03:00
case *waE2E.TemplateMessage_HydratedFourRowTemplate_HydratedTitleText:
content = fmt.Sprintf("%s\n\n%s", title.HydratedTitleText, content)
2024-09-25 16:01:35 +03:00
}
converted := mc.postProcessBusinessMessage(content, convertedTitle)
2024-09-25 16:01:35 +03:00
converted.Extra["fi.mau.whatsapp.hydrated_template_id"] = tpl.GetTemplateID()
converted.Extra["fi.mau.whatsapp.business_message_type"] = "template"
2024-09-25 16:01:35 +03:00
return converted, tplMsg.GetContextInfo()
}
func (mc *MessageConverter) convertTemplateButtonReplyMessage(ctx context.Context, msg *waE2E.TemplateButtonReplyMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: msg.GetSelectedDisplayText(),
MsgType: event.MsgText,
},
Extra: map[string]any{
"fi.mau.whatsapp.template_button_reply": map[string]any{
"id": msg.GetSelectedID(),
"index": msg.GetSelectedIndex(),
},
},
}, msg.GetContextInfo()
}
func (mc *MessageConverter) convertInteractiveMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.InteractiveMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
content := msg.GetBody().GetText()
switch interactive := msg.GetInteractiveMessage().(type) {
case *waE2E.InteractiveMessage_ShopStorefrontMessage:
content = fmt.Sprintf("%s\n\nUnsupported shop storefront message", content)
case *waE2E.InteractiveMessage_CollectionMessage_:
// There doesn't seem to be much content here, maybe it's just meant to show the body/header/footer?
case *waE2E.InteractiveMessage_NativeFlowMessage_:
if buttons := interactive.NativeFlowMessage.GetButtons(); len(buttons) > 0 {
descriptions := make([]string, len(buttons))
for i, button := range buttons {
descriptions[i] = fmt.Sprintf("<%s>", button.GetName())
}
content = fmt.Sprintf("%s\n\n%s\nUse the WhatsApp app to click buttons", content, strings.Join(descriptions, " - "))
}
case *waE2E.InteractiveMessage_CarouselMessage_:
content = fmt.Sprintf("%s\n\nUnsupported carousel message", content)
}
if footer := msg.GetFooter().GetText(); footer != "" {
content = fmt.Sprintf("%s\n\n%s", content, footer)
}
if title := msg.GetHeader().GetTitle(); title != "" {
if subtitle := msg.GetHeader().GetSubtitle(); subtitle != "" {
title = fmt.Sprintf("%s\n%s", title, subtitle)
}
content = fmt.Sprintf("%s\n\n%s", title, content)
}
var convertedTitle *bridgev2.ConvertedMessagePart
switch headerMedia := msg.GetHeader().GetMedia().(type) {
case *waE2E.InteractiveMessage_Header_DocumentMessage:
convertedTitle, _ = mc.convertMediaMessage(ctx, headerMedia.DocumentMessage, "file attachment", info, false, nil)
case *waE2E.InteractiveMessage_Header_ImageMessage:
convertedTitle, _ = mc.convertMediaMessage(ctx, headerMedia.ImageMessage, "photo", info, false, nil)
case *waE2E.InteractiveMessage_Header_VideoMessage:
convertedTitle, _ = mc.convertMediaMessage(ctx, headerMedia.VideoMessage, "video attachment", info, false, nil)
case *waE2E.InteractiveMessage_Header_LocationMessage:
content = fmt.Sprintf("Unsupported location message\n\n%s", content)
case *waE2E.InteractiveMessage_Header_ProductMessage:
content = fmt.Sprintf("Unsupported product message\n\n%s", content)
case *waE2E.InteractiveMessage_Header_JPEGThumbnail:
content = fmt.Sprintf("Unsupported thumbnail message\n\n%s", content)
}
converted := mc.postProcessBusinessMessage(content, convertedTitle)
converted.Extra["fi.mau.whatsapp.business_message_type"] = "interactive"
return converted, msg.GetContextInfo()
}
func (mc *MessageConverter) convertInteractiveResponseMessage(ctx context.Context, msg *waE2E.InteractiveResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: msg.GetBody().GetText(),
MsgType: event.MsgText,
},
Extra: map[string]any{
"fi.mau.whatsapp.interactive_response_message": map[string]any{},
},
}, msg.GetContextInfo()
}
func (mc *MessageConverter) convertButtonsMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.ButtonsMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
content := msg.GetContentText()
if buttons := msg.GetButtons(); len(buttons) > 0 {
descriptions := make([]string, len(buttons))
for i, button := range buttons {
descriptions[i] = fmt.Sprintf("<%s>", button.GetButtonText().GetDisplayText())
}
content = fmt.Sprintf("%s\n\n%s\nUse the WhatsApp app to click buttons", content, strings.Join(descriptions, " - "))
}
if footer := msg.GetFooterText(); footer != "" {
content = fmt.Sprintf("%s\n\n%s", content, footer)
}
var convertedHeader *bridgev2.ConvertedMessagePart
switch header := msg.GetHeader().(type) {
case *waE2E.ButtonsMessage_DocumentMessage:
convertedHeader, _ = mc.convertMediaMessage(ctx, header.DocumentMessage, "file attachment", info, false, nil)
case *waE2E.ButtonsMessage_ImageMessage:
convertedHeader, _ = mc.convertMediaMessage(ctx, header.ImageMessage, "photo", info, false, nil)
case *waE2E.ButtonsMessage_VideoMessage:
convertedHeader, _ = mc.convertMediaMessage(ctx, header.VideoMessage, "video attachment", info, false, nil)
case *waE2E.ButtonsMessage_LocationMessage:
content = fmt.Sprintf("Unsupported location message\n\n%s", content)
case *waE2E.ButtonsMessage_Text:
content = fmt.Sprintf("%s\n\n%s", header.Text, content)
}
converted := mc.postProcessBusinessMessage(content, convertedHeader)
converted.Extra["fi.mau.whatsapp.business_message_type"] = "buttons"
return converted, msg.GetContextInfo()
}
func (mc *MessageConverter) convertButtonsResponseMessage(ctx context.Context, msg *waE2E.ButtonsResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: msg.GetSelectedDisplayText(),
MsgType: event.MsgText,
},
Extra: map[string]any{
"fi.mau.whatsapp.buttons_response_message": map[string]any{
"selected_button_id": msg.GetSelectedButtonID(),
},
},
}, msg.GetContextInfo()
}
func (mc *MessageConverter) postProcessBusinessMessage(content string, headerMediaPart *bridgev2.ConvertedMessagePart) *bridgev2.ConvertedMessagePart {
2024-09-25 16:01:35 +03:00
converted := &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: content,
2024-09-25 16:01:35 +03:00
},
}
mc.parseFormatting(converted.Content, true, false)
if headerMediaPart != nil {
converted.Content = headerMediaPart.Content
converted.Extra = headerMediaPart.Extra
converted.DBMetadata = headerMediaPart.DBMetadata
if content != "" {
if converted.Content.FileName == "" || converted.Content.FileName == converted.Content.Body {
converted.Content.FileName = converted.Content.Body
converted.Content.Body = ""
}
if converted.Content.Body != "" {
converted.Content.Body += "\n\n"
}
converted.Content.Body += content
contentHTML := parseWAFormattingToHTML(content, true)
if contentHTML != event.TextToHTML(content) || converted.Content.FormattedBody != "" {
converted.Content.Format = event.FormatHTML
if converted.Content.FormattedBody != "" {
converted.Content.FormattedBody += "<br><br>"
}
converted.Content.FormattedBody += contentHTML
}
}
}
if converted.Extra == nil {
converted.Extra = make(map[string]any)
}
return converted
}
func (mc *MessageConverter) convertListMessage(ctx context.Context, msg *waE2E.ListMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
2024-09-25 16:01:35 +03:00
body := msg.GetDescription()
if msg.GetTitle() != "" {
if body == "" {
body = msg.GetTitle()
} else {
body = fmt.Sprintf("%s\n\n%s", msg.GetTitle(), body)
}
}
randomID := random.String(64)
body = fmt.Sprintf("%s\n%s", body, randomID)
if msg.GetFooterText() != "" {
body = fmt.Sprintf("%s\n\n%s", body, msg.GetFooterText())
}
converted := &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: body,
MsgType: event.MsgText,
},
Extra: map[string]any{
"fi.mau.whatsapp.business_message_type": "list",
},
}
mc.parseFormatting(converted.Content, false, true)
2024-09-25 16:01:35 +03:00
var optionsMarkdown strings.Builder
_, _ = fmt.Fprintf(&optionsMarkdown, "#### %s\n", msg.GetButtonText())
for _, section := range msg.GetSections() {
nesting := ""
if section.GetTitle() != "" {
_, _ = fmt.Fprintf(&optionsMarkdown, "* %s\n", section.GetTitle())
nesting = " "
}
for _, row := range section.GetRows() {
if row.GetDescription() != "" {
_, _ = fmt.Fprintf(&optionsMarkdown, "%s* %s: %s\n", nesting, row.GetTitle(), row.GetDescription())
} else {
_, _ = fmt.Fprintf(&optionsMarkdown, "%s* %s\n", nesting, row.GetTitle())
}
}
}
optionsMarkdown.WriteString("\nUse the WhatsApp app to respond")
rendered := format.RenderMarkdown(optionsMarkdown.String(), true, false)
converted.Content.Body = strings.Replace(converted.Content.Body, randomID, rendered.Body, 1)
converted.Content.FormattedBody = strings.Replace(converted.Content.FormattedBody, randomID, rendered.FormattedBody, 1)
return converted, msg.GetContextInfo()
}
func (mc *MessageConverter) convertListResponseMessage(ctx context.Context, msg *waE2E.ListResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
var body string
if msg.GetTitle() != "" {
if msg.GetDescription() != "" {
body = fmt.Sprintf("%s\n\n%s", msg.GetTitle(), msg.GetDescription())
} else {
body = msg.GetTitle()
}
} else if msg.GetDescription() != "" {
body = msg.GetDescription()
} else {
body = "Unsupported list reply message"
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: body,
MsgType: event.MsgText,
},
Extra: map[string]any{
"fi.mau.whatsapp.list_reply": map[string]any{
"row_id": msg.GetSingleSelectReply().GetSelectedRowID(),
},
},
}, msg.GetContextInfo()
}