Skip to content

Commit

Permalink
Implement URL unfurling
Browse files Browse the repository at this point in the history
  • Loading branch information
ilmotta committed May 10, 2023
1 parent ff75280 commit c8315fd
Show file tree
Hide file tree
Showing 19 changed files with 1,184 additions and 32 deletions.
3 changes: 2 additions & 1 deletion exchanges/exchanges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package exchanges
import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

"github.com/ethereum/go-ethereum/common"
)

func TestNullAddress(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ require (
github.com/yeqown/go-qrcode/v2 v2.2.1
github.com/yeqown/go-qrcode/writer/standard v1.2.1
go.uber.org/multierr v1.8.0
golang.org/x/net v0.8.0
)

require (
Expand Down Expand Up @@ -257,7 +258,6 @@ require (
go.uber.org/fx v1.18.2 // indirect
golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions images/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,13 @@ func TestCompressToFileLimits(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 291645, bb.Len())
}

func TestGetPayloadFromURI(t *testing.T) {
payload, err := GetPayloadFromURI("data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=")
require.NoError(t, err)
require.Equal(
t,
[]byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50},
payload,
)
}
2 changes: 1 addition & 1 deletion protocol/chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (s *ChatTestSuite) TestSerializeJSON() {

message.From = "0x04deaafa03e3a646e54a36ec3f6968c1d3686847d88420f00c0ab6ee517ee1893398fca28aacd2af74f2654738c21d10bad3d88dc64201ebe0de5cf1e313970d3d"
message.Clock = 1
message.Text = "`some markdown text`"
message.Text = "`some markdown text` https://status.im"
s.Require().NoError(message.PrepareContent(""))
message.ParsedTextAst = nil
chat.LastMessage = message
Expand Down
130 changes: 129 additions & 1 deletion protocol/common/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"strings"
"unicode"
Expand Down Expand Up @@ -80,6 +81,25 @@ const (
ContactVerificationStateCanceled
)

type LinkPreviewThumbnail struct {
Width int `json:"width"`
Height int `json:"height"`
// Non-empty when the thumbnail is available via the media server, i.e. after
// the chat message is sent.
URL string `json:"url,omitempty"`
// Non-empty when the thumbnail payload needs to be shared with the client,
// but before it has been persisted.
DataURI string `json:"dataUri,omitempty"`
}

type LinkPreview struct {
URL string `json:"url"`
Hostname string `json:"hostname"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Thumbnail LinkPreviewThumbnail `json:"thumbnail,omitempty"`
}

const EveryoneMentionTag = "0x00001"

type CommandParameters struct {
Expand Down Expand Up @@ -197,7 +217,8 @@ type Message struct {
Replied bool `json:"replied"`

// Links is an array of links within given message
Links []string
Links []string
LinkPreviews []LinkPreview `json:"linkPreviews"`

// EditedAt indicates the clock value it was edited
EditedAt uint64 `json:"editedAt"`
Expand Down Expand Up @@ -264,6 +285,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
Mentioned bool `json:"mentioned,omitempty"`
Replied bool `json:"replied,omitempty"`
Links []string `json:"links,omitempty"`
LinkPreviews []LinkPreview `json:"linkPreviews,omitempty"`
EditedAt uint64 `json:"editedAt,omitempty"`
Deleted bool `json:"deleted,omitempty"`
DeletedBy string `json:"deletedBy,omitempty"`
Expand Down Expand Up @@ -302,6 +324,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
Mentioned: m.Mentioned,
Replied: m.Replied,
Links: m.Links,
LinkPreviews: m.LinkPreviews,
MessageType: m.MessageType,
CommandParameters: m.CommandParameters,
GapParameters: m.GapParameters,
Expand Down Expand Up @@ -522,6 +545,10 @@ type MentionsAndLinksVisitor struct {
links []string
}

type LinksVisitor struct {
Links []string
}

func (v *MentionsAndLinksVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus {
// only on entering we fetch, otherwise we go on
if !entering {
Expand All @@ -541,12 +568,31 @@ func (v *MentionsAndLinksVisitor) Visit(node ast.Node, entering bool) ast.WalkSt
return ast.GoToNext
}

func (v *LinksVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.GoToNext
}

switch n := node.(type) {
case *ast.Link:
v.Links = append(v.Links, string(n.Destination))
}

return ast.GoToNext
}

func runMentionsAndLinksVisitor(parsedText ast.Node, identity string) *MentionsAndLinksVisitor {
visitor := &MentionsAndLinksVisitor{identity: identity}
ast.Walk(parsedText, visitor)
return visitor
}

func RunLinksVisitor(parsedText ast.Node) *LinksVisitor {
visitor := &LinksVisitor{}
ast.Walk(parsedText, visitor)
return visitor
}

// PrepareContent return the parsed content of the message, the line-count and whether
// is a right-to-left message
func (m *Message) PrepareContent(identity string) error {
Expand Down Expand Up @@ -711,6 +757,88 @@ func (m *Message) LoadImage() error {
return nil
}

func isValidLinkPreviewForProto(preview LinkPreview) bool {
return preview.Title != "" && preview.URL != "" &&
((preview.Thumbnail.DataURI == "" && preview.Thumbnail.Width == 0 && preview.Thumbnail.Height == 0) ||
(preview.Thumbnail.DataURI != "" && preview.Thumbnail.Width > 0 && preview.Thumbnail.Height > 0))
}

// ConvertLinkPreviewsToProto expects previews to be correctly sent by the
// client because we can't attempt to re-unfurl URLs at this point (it's
// actually undesirable). We run a basic validation as an additional safety net.
func (m *Message) ConvertLinkPreviewsToProto() ([]*protobuf.UnfurledLink, error) {
if len(m.LinkPreviews) == 0 {
return nil, nil
}

unfurledLinks := make([]*protobuf.UnfurledLink, 0, len(m.LinkPreviews))

for _, preview := range m.LinkPreviews {
// Do not process subsequent previews because we do expect all previews to
// be valid at this stage.
if !isValidLinkPreviewForProto(preview) {
return nil, fmt.Errorf("invalid link preview, url='%s'", preview.URL)
}

var payload []byte
var err error
if preview.Thumbnail.DataURI != "" {
payload, err = images.GetPayloadFromURI(preview.Thumbnail.DataURI)
if err != nil {
return nil, fmt.Errorf("could not get data URI payload, url='%s': %w", preview.URL, err)
}
}

ul := &protobuf.UnfurledLink{
Url: preview.URL,
Title: preview.Title,
Description: preview.Description,
ThumbnailWidth: uint32(preview.Thumbnail.Width),
ThumbnailHeight: uint32(preview.Thumbnail.Height),
ThumbnailPayload: payload,
}
unfurledLinks = append(unfurledLinks, ul)
}

return unfurledLinks, nil
}

func (m *Message) ConvertFromProtoToLinkPreviews(makeMediaServerURL func(msgID string, previewURL string) string) []LinkPreview {
var links []*protobuf.UnfurledLink

if links = m.GetUnfurledLinks(); links == nil {
return nil
}

previews := make([]LinkPreview, 0, len(links))
for _, link := range links {
parsedURL, err := url.Parse(link.Url)
var hostname string
// URL parsing in Go can fail with URLs that weren't correctly URL encoded.
// This shouldn't happen in general, but if an error happens we just reuse
// the full URL.
if err != nil {
hostname = link.Url
} else {
hostname = parsedURL.Hostname()
}
lp := LinkPreview{
Description: link.Description,
Hostname: hostname,
Title: link.Title,
URL: link.Url,
}
if payload := link.GetThumbnailPayload(); payload != nil {
lp.Thumbnail.Width = int(link.ThumbnailWidth)
lp.Thumbnail.Height = int(link.ThumbnailHeight)
lp.Thumbnail.URL = makeMediaServerURL(m.ID, link.Url)
}
previews = append(previews, lp)
}

return previews
}

func (m *Message) SetAlbumIDAndImagesCount(albumID string, imagesCount uint32) error {
imageMessage := m.GetImage()
if imageMessage == nil {
Expand Down
Loading

0 comments on commit c8315fd

Please sign in to comment.