Ver código fonte

Add support for asking homeserver for URL previews

Tulir Asokan 3 anos atrás
pai
commit
10a7c781e6
8 arquivos alterados com 74 adições e 48 exclusões
  1. 2 0
      CHANGELOG.md
  2. 1 0
      config/bridge.go
  3. 1 0
      config/upgrade.go
  4. 4 0
      example-config.yaml
  5. 4 3
      go.mod
  6. 5 3
      go.sum
  7. 8 8
      portal.go
  8. 49 34
      urlpreview.go

+ 2 - 0
CHANGELOG.md

@@ -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)
 

+ 1 - 0
config/bridge.go

@@ -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"`
 

+ 1 - 0
config/upgrade.go

@@ -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")

+ 4 - 0
example-config.yaml

@@ -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"

+ 4 - 3
go.mod

@@ -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
 )

+ 5 - 3
go.sum

@@ -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=

+ 8 - 8
portal.go

@@ -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:

+ 49 - 34
urlpreview.go

@@ -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
-	}
-	var previews []BeeperLinkPreview
-	if err := json.Unmarshal([]byte(rawPreview.Raw), &previews); err != nil || len(previews) == 0 {
-		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,
+			}
+		}
 	}
-	// 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
 }