Add support for asking homeserver for URL previews

This commit is contained in:
Tulir Asokan 2022-02-15 16:28:20 +02:00
parent d668c031f7
commit 10a7c781e6
8 changed files with 74 additions and 48 deletions

View file

@ -5,11 +5,13 @@
* (Re-)Added support for setting group avatar from Matrix.
* Added initial support for re-fetching old media from phone.
* Added support for bridging audio message waveforms in both directions.
* Added support for sending URL previews to WhatsApp (both custom and autogenerated).
* Fixed some issues with read receipt bridging
* Fixed `!wa open` not working with new-style group IDs.
* Fixed panic in disappearing message handling code if a portal is deleted with
messages still inside.
* Fixed disappearing message timer not being stored in post-login history sync.
* Fixed formatting not being parsed in most incoming WhatsApp messages.
# v0.2.3 (2022-01-16)

View file

@ -72,6 +72,7 @@ type BridgeConfig struct {
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
AllowUserInvite bool `yaml:"allow_user_invite"`
FederateRooms bool `yaml:"federate_rooms"`
URLPreviews bool `yaml:"url_previews"`
DisappearingMessagesInGroups bool `yaml:"disappearing_messages_in_groups"`

View file

@ -104,6 +104,7 @@ func (helper *UpgradeHelper) doUpgrade() {
helper.Copy(Bool, "bridge", "federate_rooms")
helper.Copy(Bool, "bridge", "disappearing_messages_in_groups")
helper.Copy(Bool, "bridge", "disable_bridge_alerts")
helper.Copy(Bool, "bridge", "url_previews")
helper.Copy(Str, "bridge", "management_room_text", "welcome")
helper.Copy(Str, "bridge", "management_room_text", "welcome_connected")
helper.Copy(Str, "bridge", "management_room_text", "welcome_unconnected")

View file

@ -194,6 +194,10 @@ bridge:
# Should the bridge never send alerts to the bridge management room?
# These are mostly things like the user being logged out.
disable_bridge_alerts: false
# Should the bridge detect URLs in outgoing messages, ask the homeserver to generate a preview,
# and send it to WhatsApp? URL previews can always be sent using the `com.beeper.linkpreviews`
# key in the event content even if this is disabled.
url_previews: false
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!wa"

7
go.mod
View file

@ -12,11 +12,12 @@ require (
github.com/tidwall/gjson v1.14.0
go.mau.fi/whatsmeow v0.0.0-20220211173754-90c655671ab0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
maunium.net/go/mauflag v1.0.0
maunium.net/go/maulogger/v2 v2.3.2
maunium.net/go/mautrix v0.10.11-0.20220215121349-628a694b037f
maunium.net/go/mautrix v0.10.11-0.20220215142712-441b0812745a
)
require (
@ -33,8 +34,8 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.4 // indirect
go.mau.fi/libsignal v0.0.0-20211109153248-a67163214910 // indirect
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

8
go.sum
View file

@ -126,8 +126,9 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506 h1:EuGTJDfeg/PGZJp3gq1K+14eSLFTsrj1eg8KQuiUyKg=
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -167,6 +168,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
@ -199,5 +201,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.10.11-0.20220215121349-628a694b037f h1:07t3qJkxqu7iQZ7OUIba7NkwyfpD6ul8GOjsM+Nemh0=
maunium.net/go/mautrix v0.10.11-0.20220215121349-628a694b037f/go.mod h1:Ynac6y32yvdJC8YiYvWjWp6u1WjVTNq+JssC+07ZZWw=
maunium.net/go/mautrix v0.10.11-0.20220215142712-441b0812745a h1:qemTkoULb98wqW1CrV0qD1SQZ4rQw6HgmIuzYyJ3N64=
maunium.net/go/mautrix v0.10.11-0.20220215142712-441b0812745a/go.mod h1:Ynac6y32yvdJC8YiYvWjWp6u1WjVTNq+JssC+07ZZWw=

View file

@ -2492,14 +2492,14 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
if content.MsgType == event.MsgEmote && !relaybotFormatted {
text = "/me " + text
}
if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil || ctxInfo.Expiration != nil || evt.Content.Raw["com.beeper.linkpreviews"] != nil {
msg.ExtendedTextMessage = &waProto.ExtendedTextMessage{
Text: &text,
ContextInfo: &ctxInfo,
}
portal.convertURLPreviewToWhatsApp(sender, evt, msg.ExtendedTextMessage)
} else {
msg.ExtendedTextMessage = &waProto.ExtendedTextMessage{
Text: &text,
ContextInfo: &ctxInfo,
}
hasPreview := portal.convertURLPreviewToWhatsApp(sender, evt, msg.ExtendedTextMessage)
if ctxInfo.StanzaId == nil && ctxInfo.MentionedJid == nil && ctxInfo.Expiration == nil && !hasPreview {
// No need for extended message
msg.ExtendedTextMessage = nil
msg.Conversation = &text
}
case event.MsgImage:

View file

@ -22,34 +22,27 @@ import (
"encoding/json"
"image"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/tidwall/gjson"
"golang.org/x/net/idna"
"google.golang.org/protobuf/proto"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/whatsmeow"
waProto "go.mau.fi/whatsmeow/binary/proto"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event"
)
type BeeperLinkPreview struct {
MatchedURL string `json:"matched_url"`
CanonicalURL string `json:"og:url,omitempty"`
Title string `json:"og:title,omitempty"`
Type string `json:"og:type,omitempty"`
Description string `json:"og:description,omitempty"`
ImageURL id.ContentURIString `json:"og:image,omitempty"`
mautrix.RespPreviewURL
MatchedURL string `json:"matched_url"`
ImageEncryption *event.EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
ImageSize int `json:"matrix:image:size,omitempty"`
ImageWidth int `json:"og:image:width,omitempty"`
ImageHeight int `json:"og:image:height,omitempty"`
ImageType string `json:"og:image:type,omitempty"`
}
func (portal *Portal) convertURLPreviewToBeeper(intent *appservice.IntentAPI, source *User, msg *waProto.ExtendedTextMessage) []*BeeperLinkPreview {
@ -58,10 +51,12 @@ func (portal *Portal) convertURLPreviewToBeeper(intent *appservice.IntentAPI, so
}
output := &BeeperLinkPreview{
MatchedURL: msg.GetMatchedText(),
CanonicalURL: msg.GetCanonicalUrl(),
Title: msg.GetTitle(),
Description: msg.GetDescription(),
MatchedURL: msg.GetMatchedText(),
RespPreviewURL: mautrix.RespPreviewURL{
CanonicalURL: msg.GetCanonicalUrl(),
Title: msg.GetTitle(),
Description: msg.GetDescription(),
},
}
if len(output.CanonicalURL) == 0 {
output.CanonicalURL = output.MatchedURL
@ -115,19 +110,38 @@ func (portal *Portal) convertURLPreviewToBeeper(intent *appservice.IntentAPI, so
return []*BeeperLinkPreview{output}
}
func (portal *Portal) convertURLPreviewToWhatsApp(sender *User, evt *event.Event, dest *waProto.ExtendedTextMessage) {
var URLRegex = regexp.MustCompile(`https?://[^\s/_*]+(?:/\S*)?`)
func (portal *Portal) convertURLPreviewToWhatsApp(sender *User, evt *event.Event, dest *waProto.ExtendedTextMessage) bool {
var preview *BeeperLinkPreview
rawPreview := gjson.GetBytes(evt.Content.VeryRaw, `com\.beeper\.linkpreviews`)
if !rawPreview.Exists() || !rawPreview.IsArray() {
return
if rawPreview.Exists() && rawPreview.IsArray() {
var previews []BeeperLinkPreview
if err := json.Unmarshal([]byte(rawPreview.Raw), &previews); err != nil || len(previews) == 0 {
return false
}
// WhatsApp only supports a single preview.
preview = &previews[0]
} else if portal.bridge.Config.Bridge.URLPreviews {
if matchedURL := URLRegex.FindString(evt.Content.AsMessage().Body); len(matchedURL) == 0 {
return false
} else if parsed, err := url.Parse(matchedURL); err != nil {
return false
} else if parsed.Host, err = idna.ToASCII(parsed.Host); err != nil {
return false
} else if mxPreview, err := portal.MainIntent().GetURLPreview(parsed.String()); err != nil {
portal.log.Warnfln("Failed to fetch preview for %s: %v", matchedURL, err)
return false
} else {
preview = &BeeperLinkPreview{
RespPreviewURL: *mxPreview,
MatchedURL: matchedURL,
}
}
}
var previews []BeeperLinkPreview
if err := json.Unmarshal([]byte(rawPreview.Raw), &previews); err != nil || len(previews) == 0 {
return
}
// WhatsApp only supports a single preview.
preview := previews[0]
if len(preview.MatchedURL) == 0 {
return
if preview == nil || len(preview.MatchedURL) == 0 {
return false
}
dest.MatchedText = &preview.MatchedURL
@ -151,20 +165,20 @@ func (portal *Portal) convertURLPreviewToWhatsApp(sender *User, evt *event.Event
data, err := portal.MainIntent().DownloadBytes(imageMXC)
if err != nil {
portal.log.Errorfln("Failed to download URL preview image %s in %s: %v", preview.ImageURL, evt.ID, err)
return
return true
}
if preview.ImageEncryption != nil {
data, err = preview.ImageEncryption.Decrypt(data)
if err != nil {
portal.log.Errorfln("Failed to decrypt URL preview image in %s: %v", evt.ID, err)
return
return true
}
}
dest.MediaKeyTimestamp = proto.Int64(time.Now().Unix())
uploadResp, err := sender.Client.Upload(context.Background(), data, whatsmeow.MediaLinkThumbnail)
if err != nil {
portal.log.Errorfln("Failed to upload URL preview thumbnail in %s: %v", evt.ID, err)
return
return true
}
dest.ThumbnailSha256 = uploadResp.FileSHA256
dest.ThumbnailEncSha256 = uploadResp.FileEncSHA256
@ -183,4 +197,5 @@ func (portal *Portal) convertURLPreviewToWhatsApp(sender *User, evt *event.Event
dest.ThumbnailHeight = proto.Uint32(uint32(height))
}
}
return true
}