diff --git a/example-config.yaml b/example-config.yaml index bdf51f7..49ae665 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -94,7 +94,7 @@ bridge: # The following variables are also available, but will cause problems on multi-user instances: # {{.FullName}} - full name from contact list # {{.FirstName}} - first name from contact list - displayname_template: "{{if .PushName}}{{.PushName}}{{else if .BusinessName}}{{.BusinessName}}{{else}}{{.JID}}{{end}} (WA)" + displayname_template: "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)" # Should the bridge create a space for each logged-in user and add bridged rooms to it? # Users who logged in before turning this on should run `!wa sync space` to create and fill the space for the first time. personal_filtering_spaces: false diff --git a/formatting.go b/formatting.go index e99d186..9a6dbec 100644 --- a/formatting.go +++ b/formatting.go @@ -33,6 +33,7 @@ var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)") var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)") var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)") var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```") +var inlineURLRegex = regexp.MustCompile(`\[(.+?)]\((.+?)\)`) const mentionedJIDsContextKey = "net.maunium.whatsapp.mentioned_jids" @@ -108,7 +109,7 @@ func (formatter *Formatter) getMatrixInfoByJID(roomID id.RoomID, jid types.JID) return } -func (formatter *Formatter) ParseWhatsApp(roomID id.RoomID, content *event.MessageEventContent, mentionedJIDs []string) { +func (formatter *Formatter) ParseWhatsApp(roomID id.RoomID, content *event.MessageEventContent, mentionedJIDs []string, allowInlineURL bool) { output := html.EscapeString(content.Body) for regex, replacement := range formatter.waReplString { output = regex.ReplaceAllString(output, replacement) @@ -116,6 +117,12 @@ func (formatter *Formatter) ParseWhatsApp(roomID id.RoomID, content *event.Messa for regex, replacer := range formatter.waReplFunc { output = regex.ReplaceAllStringFunc(output, replacer) } + if allowInlineURL { + output = inlineURLRegex.ReplaceAllStringFunc(output, func(s string) string { + groups := inlineURLRegex.FindStringSubmatch(s) + return fmt.Sprintf(`%s`, groups[2], groups[1]) + }) + } for _, rawJID := range mentionedJIDs { jid, err := types.ParseJID(rawJID) if err != nil { diff --git a/go.mod b/go.mod index 3584e9e..f916001 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/prometheus/client_golang v1.12.2-0.20220613221938-ebd77f036066 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/gjson v1.14.1 - go.mau.fi/whatsmeow v0.0.0-20220622155743-bfb10aff31b9 + go.mau.fi/whatsmeow v0.0.0-20220624184947-57a69a641154 golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 golang.org/x/net v0.0.0-20220513224357-95641704303c google.golang.org/protobuf v1.28.0 diff --git a/go.sum b/go.sum index 7e81191..892d717 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mau.fi/libsignal v0.0.0-20220425070825-c40c839ee6a0 h1:3IQF2bgAyibdo77hTejwuJe4jlypj9QaE4xCQuxrThM= go.mau.fi/libsignal v0.0.0-20220425070825-c40c839ee6a0/go.mod h1:kBOXTvYyDG/q1Ihgvd4J6WenGPh7wtEGvPKF6vmf5ak= -go.mau.fi/whatsmeow v0.0.0-20220622155743-bfb10aff31b9 h1:XJhWnHI8dB2oSNwpfbpbg1jpeLWKl0aht8fy/vo50ew= -go.mau.fi/whatsmeow v0.0.0-20220622155743-bfb10aff31b9/go.mod h1:iUBgOLNaqShLrR17u0kIiRptIGFH+nbT1tRhaWBEX/c= +go.mau.fi/whatsmeow v0.0.0-20220624184947-57a69a641154 h1:jUe0Re+w8/YHfxYryxjVkG3PEQDujCzGhbqsk6Qadtg= +go.mau.fi/whatsmeow v0.0.0-20220624184947-57a69a641154/go.mod h1:iUBgOLNaqShLrR17u0kIiRptIGFH+nbT1tRhaWBEX/c= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/portal.go b/portal.go index e457d53..f068b7b 100644 --- a/portal.go +++ b/portal.go @@ -351,7 +351,8 @@ func containsSupportedMessage(waMsg *waProto.Message) bool { return waMsg.Conversation != nil || waMsg.ExtendedTextMessage != nil || waMsg.ImageMessage != nil || waMsg.StickerMessage != nil || waMsg.AudioMessage != nil || waMsg.VideoMessage != nil || waMsg.DocumentMessage != nil || waMsg.ContactMessage != nil || waMsg.LocationMessage != nil || - waMsg.LiveLocationMessage != nil || waMsg.GroupInviteMessage != nil || waMsg.ContactsArrayMessage != nil + waMsg.LiveLocationMessage != nil || waMsg.GroupInviteMessage != nil || waMsg.ContactsArrayMessage != nil || + waMsg.HighlyStructuredMessage != nil || waMsg.TemplateMessage != nil } func getMessageType(waMsg *waProto.Message) string { @@ -483,6 +484,10 @@ func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User, switch { case waMsg.Conversation != nil || waMsg.ExtendedTextMessage != nil: return portal.convertTextMessage(intent, source, waMsg) + case waMsg.TemplateMessage != nil: + return portal.convertTemplateMessage(intent, source, info, waMsg.GetTemplateMessage()) + case waMsg.HighlyStructuredMessage != nil: + return portal.convertTemplateMessage(intent, source, info, waMsg.GetHighlyStructuredMessage().GetHydratedHsm()) case waMsg.ImageMessage != nil: return portal.convertMediaMessage(intent, source, info, waMsg.GetImageMessage(), "photo", isBackfill) case waMsg.StickerMessage != nil: @@ -1710,7 +1715,7 @@ func (portal *Portal) convertTextMessage(intent *appservice.IntentAPI, source *U } contextInfo := msg.GetExtendedTextMessage().GetContextInfo() - portal.bridge.Formatter.ParseWhatsApp(portal.MXID, content, contextInfo.GetMentionedJid()) + portal.bridge.Formatter.ParseWhatsApp(portal.MXID, content, contextInfo.GetMentionedJid(), false) replyTo := contextInfo.GetStanzaId() expiresIn := contextInfo.GetExpiration() extraAttrs := map[string]interface{}{} @@ -1726,6 +1731,77 @@ func (portal *Portal) convertTextMessage(intent *appservice.IntentAPI, source *U } } +func (portal *Portal) convertTemplateMessage(intent *appservice.IntentAPI, source *User, info *types.MessageInfo, tplMsg *waProto.TemplateMessage) *ConvertedMessage { + converted := &ConvertedMessage{ + Intent: intent, + Type: event.EventMessage, + Content: &event.MessageEventContent{ + Body: "Unsupported business message", + MsgType: event.MsgText, + }, + ReplyTo: tplMsg.GetContextInfo().GetStanzaId(), + ExpiresIn: tplMsg.GetContextInfo().GetExpiration(), + } + + tpl := tplMsg.GetHydratedTemplate() + if tpl == nil { + return converted + } + content := tpl.GetHydratedContentText() + 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 *waProto.HydratedTemplateButton_QuickReplyButton: + descriptions[i] = fmt.Sprintf("<%s>", button.QuickReplyButton.GetDisplayText()) + addButtonText = true + case *waProto.HydratedTemplateButton_UrlButton: + descriptions[i] = fmt.Sprintf("[%s](%s)", button.UrlButton.GetDisplayText(), button.UrlButton.GetUrl()) + case *waProto.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 = fmt.Sprintf("%s\n\n%s", content, description) + } + if footer := tpl.GetHydratedFooterText(); footer != "" { + content = fmt.Sprintf("%s\n\n%s", content, footer) + } + + var convertedTitle *ConvertedMessage + switch title := tpl.GetTitle().(type) { + case *waProto.HydratedFourRowTemplate_DocumentMessage: + convertedTitle = portal.convertMediaMessage(intent, source, info, title.DocumentMessage, "file attachment", false) + case *waProto.HydratedFourRowTemplate_ImageMessage: + convertedTitle = portal.convertMediaMessage(intent, source, info, title.ImageMessage, "photo", false) + case *waProto.HydratedFourRowTemplate_VideoMessage: + convertedTitle = portal.convertMediaMessage(intent, source, info, title.VideoMessage, "video attachment", false) + case *waProto.HydratedFourRowTemplate_LocationMessage: + content = fmt.Sprintf("Unsupported location message\n\n%s", content) + case *waProto.HydratedFourRowTemplate_HydratedTitleText: + content = fmt.Sprintf("%s\n\n%s", title.HydratedTitleText, content) + } + + converted.Content.Body = content + portal.bridge.Formatter.ParseWhatsApp(portal.MXID, converted.Content, nil, true) + if convertedTitle != nil { + converted.MediaKey = convertedTitle.MediaKey + converted.Extra = convertedTitle.Extra + converted.Caption = converted.Content + converted.Content = convertedTitle.Content + converted.Error = convertedTitle.Error + } + if converted.Extra == nil { + converted.Extra = make(map[string]interface{}) + } + converted.Extra["fi.mau.whatsapp.hydrated_template_id"] = tpl.GetTemplateId() + return converted +} + func (portal *Portal) convertLiveLocationMessage(intent *appservice.IntentAPI, msg *waProto.LiveLocationMessage) *ConvertedMessage { content := &event.MessageEventContent{ Body: "Started sharing live location", @@ -2232,7 +2308,7 @@ func (portal *Portal) convertMediaMessageContent(intent *appservice.IntentAPI, m MsgType: event.MsgNotice, } - portal.bridge.Formatter.ParseWhatsApp(portal.MXID, captionContent, msg.GetContextInfo().GetMentionedJid()) + portal.bridge.Formatter.ParseWhatsApp(portal.MXID, captionContent, msg.GetContextInfo().GetMentionedJid(), false) } return &ConvertedMessage{ diff --git a/user.go b/user.go index 2ce53d1..63bd3f2 100644 --- a/user.go +++ b/user.go @@ -721,6 +721,8 @@ func (user *User) HandleEvent(event interface{}) { go user.syncPuppet(v.JID, "contact event") case *events.PushName: go user.syncPuppet(v.JID, "push name event") + case *events.BusinessName: + go user.syncPuppet(v.JID, "business name event") case *events.GroupInfo: user.groupListCache = nil go user.handleGroupUpdate(v)