diff --git a/protocol/common/message.go b/protocol/common/message.go index 7859b9870e3..ef490c14a78 100644 --- a/protocol/common/message.go +++ b/protocol/common/message.go @@ -943,7 +943,7 @@ func (preview *LinkPreviewThumbnail) ConvertToProto() (*protobuf.UnfurledLinkThu }, nil } -func (m *Message) ConvertStatusLinkPreviewsToProto() ([]*protobuf.UnfurledStatusLink, error) { +func (m *Message) ConvertStatusLinkPreviewsToProto() (*protobuf.UnfurledStatusLinks, error) { if len(m.StatusLinkPreviews) == 0 { return nil, nil } @@ -1023,21 +1023,26 @@ func (m *Message) ConvertStatusLinkPreviewsToProto() ([]*protobuf.UnfurledStatus unfurledLinks = append(unfurledLinks, ul) } - return unfurledLinks, nil + return &protobuf.UnfurledStatusLinks{UnfurledStatusLinks: unfurledLinks}, nil } -func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(msgID string, previewURL string) string) []StatusLinkPreview { - var links []*protobuf.UnfurledStatusLink +func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(msgID string, previewURL string, imageId string) string) []StatusLinkPreview { + + if m.GetUnfurledStatusLinks() == nil { + return nil + } + + links := m.UnfurledStatusLinks.GetUnfurledStatusLinks() - if links = m.GetUnfurledStatusLinks(); links == nil { + if links == nil { return nil } - createThumbnail := func(thumbnail *protobuf.UnfurledLinkThumbnail, URL string) LinkPreviewThumbnail { + createThumbnail := func(thumbnail *protobuf.UnfurledLinkThumbnail, URL string, imageId string) LinkPreviewThumbnail { return LinkPreviewThumbnail{ Width: int(thumbnail.Width), Height: int(thumbnail.Height), - URL: makeMediaServerURL(m.ID, URL), + URL: makeMediaServerURL(m.ID, URL, imageId), } } @@ -1054,7 +1059,7 @@ func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(m Description: c.Description, } if icon := c.GetIcon(); icon != nil { - lp.Contact.Icon = createThumbnail(icon, link.Url) + lp.Contact.Icon = createThumbnail(icon, link.Url, "contact-icon") } } @@ -1068,10 +1073,10 @@ func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(m TagIndices: c.TagIndices, } if icon := c.GetIcon(); icon != nil { - lp.Community.Icon = createThumbnail(icon, link.Url) + lp.Community.Icon = createThumbnail(icon, link.Url, "community-icon") } if banner := c.GetBanner(); banner != nil { - lp.Community.Banner = createThumbnail(banner, link.Url) + lp.Community.Banner = createThumbnail(banner, link.Url, "community-banner") } } @@ -1084,7 +1089,10 @@ func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(m Color: c.Color, } if icon := c.GetCommunityIcon(); icon != nil { - lp.Channel.CommunityIcon = createThumbnail(icon, link.Url) + lp.Channel.CommunityIcon = createThumbnail(icon, link.Url, "channel-community-icon") + } + if banner := c.GetCommunityBanner(); banner != nil { + lp.Channel.CommunityBanner = createThumbnail(banner, link.Url, "channel-community-banner") } } diff --git a/protocol/common/message_test.go b/protocol/common/message_test.go index 0b1463a8e8c..d26ed55a07c 100644 --- a/protocol/common/message_test.go +++ b/protocol/common/message_test.go @@ -17,10 +17,6 @@ import ( const expectedJPEG = "data:image/jpeg;base64,/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=" const expectedAAC = "data:audio/aac;base64,//FQgBw//NoATGF2YzUyLjcwLjAAQniptokphEFCg5qs1v9fn48+qz1rfWNhwvz+CqB5dipmq3T2PlT1Ld6sPj+19fUt1C3NKV0KowiqohZVCrdf19WMatvV3YbIvAuy/q2RafA8UiZPmZY7DdmHZtP9ri25kedWSiMKQRt79ttlod55LkuX7/f7/f7/f7/YGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYHNqo8g5qs1v9fn48+qz1rfWNhwvz+CqAAAAAAAAAAAAAAAAAAAAAAABw//FQgCNf/CFXbUZfDKFRgsYlKDegtXJH9eLkT54uRM1ckDYDcXRzZGF6Kz5Yps5fTeLY6w7gclwly+0PJL3udY3PyekTFI65bdniF3OjvHeafzZfWTs0qRMSkdll1sbb4SNT5e8vX98ytot6jEZ0NhJi2pBVP/tKV2JMyo36n9uxR2tKR+FoLCsP4SVi49kmvaSCWm5bQD96OmVQA9Q40bqnOa7rT8j9N0TlK991XdcenGTLbyS6eUnN2U1ckf14uRPni5EzVyQAAAAAAAAAAx6Q1flBp+KH2LhgH2Xx+14QB2/jcizm6ngck4vB9DoH9/Vcb7E8Dy+D/1ii1pSPwsUUUXCSsXHsk17SBfKwn2uHr6QAAAAAAAHA//FQgBt//CF3VO1KFCFWcd/r04m+O0758/tXHUlvaqEK9lvhUZXEZMXKMV/LQ6B3/mOl/Mrfs6jpD2b7f+n4yt+tm2x5ZmnpD++dZo/V9VgblI3OW/s1b8qt0h1RBiIRIIYIYQIBeCM8yy7etkwt1JAajRSoZGwwNZ07TTFTyMR1mTUVVUTW97vaDaHU5DV1snBf0mN4fraa+rf/vpdZ8FxqatGjNxPh35UuVfpNqc48W4nZ6rOO/16cTfHad8+f2rjqS3tVAAAAAAAAAAAAAAAAAAAAAAAAAAAO//FQgBm//CEXVPU+GiFsPr7x6+N6v+m+q511I4SgtYVyoyWjcMWMxkaxxDGSx1qVcarjDESt8zLQehx/lkil/GrHBy/NfJcHek0XtfanZJLHNXO2rUnFklPAlQSBS4l0pIoXIfORcXx0UYj1nTsSe1/0wXDkkFCfxWHtqRayOmWm3oS6JGdnZdtjesjByefiS8dLW1tVVVC58ijoxN3gmGFYj07+YJ6eth9fePXxvV/031XOupHCUAAAAAAAAAAAAAAAAAAAAAAAAAAA4P/xUIAcf/whN1T9NsMOEK5rxxxxXnid+f0/Ia195vi6oGH1ZVr6kjqScdSF9lt3qXH+Lxf0fo/Oe53r99IUPzybv/YWGZ7Vgk31MGw+DMp05+3y9fPERUTHlt1c9sUyoqCaD5bdXVz2wkG0hnpDmFy8r0fr3VBn/C7Rmg+L0/45EWfdocGq3HQ1uRro0GJK+vsvo837NR82s01l/n97rsWn7RYNBM3WRcDY3cJKosqMJhgdHtj9yflthd65rxxxxXnid+f0/Ia195vi6oAAAAAAAAAAAAAAAAAAAAAAAAAAAABw" -func linkPreviewUrlMaker(msgID string, linkURL string) string { - return "https://localhost:6666/" + msgID + "-" + linkURL -} - func TestPrepareContentImage(t *testing.T) { file, err := os.Open("../../_assets/tests/test.jpg") require.NoError(t, err) @@ -213,7 +209,11 @@ func TestConvertFromProtoToLinkPreviews(t *testing.T) { }, } - previews := msg.ConvertFromProtoToLinkPreviews(linkPreviewUrlMaker) + urlMaker := func(msgID string, linkURL string) string { + return "https://localhost:6666/" + msgID + "-" + linkURL + } + + previews := msg.ConvertFromProtoToLinkPreviews(urlMaker) require.Len(t, previews, 1) p := previews[0] require.Equal(t, l.Type, p.Type) @@ -230,7 +230,7 @@ func TestConvertFromProtoToLinkPreviews(t *testing.T) { // Test when the URL is not parseable by url.Parse. l.Url = "postgres://user:abc{DEf1=ghi@example.com:5432/db?sslmode=require" msg.ChatMessage.UnfurledLinks = []*protobuf.UnfurledLink{l} - previews = msg.ConvertFromProtoToLinkPreviews(linkPreviewUrlMaker) + previews = msg.ConvertFromProtoToLinkPreviews(urlMaker) require.Len(t, previews, 1) p = previews[0] require.Equal(t, l.Url, p.Hostname) @@ -242,7 +242,7 @@ func TestConvertFromProtoToLinkPreviews(t *testing.T) { Url: "https://github.com", } msg.ChatMessage.UnfurledLinks = []*protobuf.UnfurledLink{l} - previews = msg.ConvertFromProtoToLinkPreviews(linkPreviewUrlMaker) + previews = msg.ConvertFromProtoToLinkPreviews(urlMaker) require.Len(t, previews, 1) p = previews[0] require.Equal(t, 0, p.Thumbnail.Height) @@ -256,16 +256,15 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) { PublicKey: "1", DisplayName: "2", Description: "3", - - //Icon: "4", - - //ThumbnailPayload: []byte(""), - //ThumbnailWidth: 100, - //ThumbnailHeight: 200, + Icon: &protobuf.UnfurledLinkThumbnail{ + Width: 100, + Height: 200, + Payload: []byte(""), + }, } l := &protobuf.UnfurledStatusLink{ - Url: "https://status.app/", + Url: "https://status.app", Payload: &protobuf.UnfurledStatusLink_Contact{ Contact: contact, }, @@ -274,23 +273,37 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) { msg := Message{ ID: "42", ChatMessage: &protobuf.ChatMessage{ - UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{l}, + UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{ + UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{l}, + }, }, } - previews := msg.ConvertFromProtoToStatusLinkPreviews(linkPreviewUrlMaker) + urlMaker := func(msgID string, linkURL string, imageID string) string { + return "https://localhost:6666/" + msgID + "-" + linkURL + "-" + imageID + } + + previews := msg.ConvertFromProtoToStatusLinkPreviews(urlMaker) require.Len(t, previews, 1) p := previews[0] require.Equal(t, l.Url, p.URL) - c := l.GetContact() + require.Nil(t, p.Community) + require.Nil(t, p.Channel) + + c := p.Contact require.NotNil(t, c) require.Equal(t, "1", c.PublicKey) - require.Equal(t, contact.PublicKey, c.GetPublicKey()) - require.Equal(t, contact.DisplayName, c.GetDisplayName()) - require.Equal(t, contact.Description, c.GetDescription()) - require.Nil(t, contact.Icon) + require.Equal(t, contact.PublicKey, c.PublicKey) + require.Equal(t, contact.DisplayName, c.DisplayName) + require.Equal(t, contact.Description, c.Description) + icon := c.Icon + require.NotNil(t, icon) + require.Equal(t, int(contact.Icon.Width), icon.Width) + require.Equal(t, int(contact.Icon.Height), icon.Height) + require.Equal(t, "", icon.DataURI) + require.Equal(t, "https://localhost:6666/42-https://status.app-contact-icon", icon.URL) } func assertMarshalAndUnmarshalJSON[T any](t *testing.T, obj *T, msgAndArgs ...any) { diff --git a/protocol/linkpreview_unfurler_status.go b/protocol/linkpreview_unfurler_status.go index e6de5c93057..78636f48913 100644 --- a/protocol/linkpreview_unfurler_status.go +++ b/protocol/linkpreview_unfurler_status.go @@ -1,6 +1,8 @@ package protocol import ( + "fmt" + "github.com/status-im/status-go/api/multiformat" "go.uber.org/zap" "github.com/status-im/status-go/images" @@ -21,31 +23,59 @@ func NewStatusUnfurler(URL string, messenger *Messenger, logger *zap.Logger) *St } } -func (u *StatusUnfurler) createContactData(contactData *ContactURLData) *common.StatusContactLinkPreview { +func buildThumbnail(image *images.IdentityImage, thumbnail *common.LinkPreviewThumbnail) error { + if image.IsEmpty() { + return nil + } + + var err error + + thumbnail.Width, thumbnail.Height, err = images.GetImageDimensions(image.Payload) + if err != nil { + return fmt.Errorf("failed to get image dimensions: %w", err) + } + + thumbnail.DataURI, err = image.GetDataURI() + if err != nil { + return fmt.Errorf("failed to get data uri: %w", err) + } + + return nil +} + +func (u *StatusUnfurler) buildContactData(contactData *ContactURLData) (*common.StatusContactLinkPreview, error) { c := new(common.StatusContactLinkPreview) c.PublicKey = contactData.PublicKey c.DisplayName = contactData.DisplayName c.Description = contactData.Description - var err error - contact := u.m.GetContactByID(contactData.PublicKey) + contactID, err := multiformat.DeserializeCompressedKey(contactData.PublicKey) + + if err != nil { + return nil, err + } + + contact := u.m.GetContactByID(contactID) + + // TODO: Should we do this? + //if contact == nil { + // if contact, err = u.m.RequestContactInfoFromMailserver(contactData.PublicKey, true); err != nil { + // u.logger.Warn("StatusUnfurler: failed to request contact info from mailserver") + // return c, err + // } + //} if contact == nil { - if contact, err = u.m.RequestContactInfoFromMailserver(contactData.PublicKey, true); err != nil { - u.logger.Warn("StatusUnfurler: failed to request contact info from mailserver") - return c - } + return c, nil } - if thumbImage, ok := contact.Images[images.SmallDimName]; ok { - if imageBase64, err := thumbImage.GetDataURI(); err == nil { - c.Icon.Width = thumbImage.Width - c.Icon.Height = thumbImage.Height - c.Icon.DataURI = imageBase64 + if image, ok := contact.Images[images.SmallDimName]; ok { + if err = buildThumbnail(&image, &c.Icon); err != nil { + u.logger.Warn("unfurling status link: failed to set thumbnail", zap.Error(err)) } } - return c + return c, nil } func (u *StatusUnfurler) Unfurl() (common.StatusLinkPreview, error) { @@ -59,20 +89,27 @@ func (u *StatusUnfurler) Unfurl() (common.StatusLinkPreview, error) { } if resp.Contact != nil { - preview.Contact = u.createContactData(resp.Contact) + preview.Contact, err = u.buildContactData(resp.Contact) } if resp.Community != nil { + // TODO: move to a separate func, finish preview.Community = new(common.StatusCommunityLinkPreview) preview.Community.DisplayName = resp.Community.DisplayName preview.Community.Description = resp.Community.Description } if resp.Channel != nil { + // TODO: move to a separate func, finish preview.Channel = new(common.StatusCommunityChannelLinkPreview) preview.Channel.DisplayName = resp.Channel.DisplayName preview.Channel.Description = resp.Channel.Description } + u.logger.Info("<<< StatusUnfurler::Unfurl", + zap.Any("contact", preview.Contact), + zap.Any("community", preview.Community), + zap.Any("channel", preview.Channel)) + return preview, nil } diff --git a/protocol/message_persistence.go b/protocol/message_persistence.go index 4404ace733b..d5ba2f4b0a3 100644 --- a/protocol/message_persistence.go +++ b/protocol/message_persistence.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "github.com/golang/protobuf/proto" "sort" "strings" @@ -419,10 +420,13 @@ func (db sqlitePersistence) tableUserMessagesScanAllFields(row scanner, message } if serializedUnfurledStatusLinks != nil { - err = json.Unmarshal(serializedUnfurledStatusLinks, &message.UnfurledStatusLinks) + // use proto.Marshal, because json.Marshal doesn't support `oneof` fields + var links protobuf.UnfurledStatusLinks + err = proto.Unmarshal(serializedUnfurledStatusLinks, &links) if err != nil { return err } + message.UnfurledStatusLinks = &links } if attachment.Id != "" { @@ -515,7 +519,8 @@ func (db sqlitePersistence) tableUserMessagesAllValues(message *common.Message) var serializedUnfurledStatusLinks []byte if links := message.GetUnfurledStatusLinks(); links != nil { - serializedUnfurledStatusLinks, err = json.Marshal(links) + // use proto.Marshal, because json.Marshal doesn't support `oneof` fields + serializedUnfurledStatusLinks, err = proto.Marshal(links) if err != nil { return nil, err } diff --git a/protocol/messenger_linkpreview.go b/protocol/messenger_linkpreview.go index 97fa4360227..b37be29619f 100644 --- a/protocol/messenger_linkpreview.go +++ b/protocol/messenger_linkpreview.go @@ -165,5 +165,7 @@ func (m *Messenger) UnfurlURLs(httpClient *http.Client, urls []string) (UnfurlUR r.LinkPreviews = append(r.LinkPreviews, p) } + m.logger.Info("<<< UnfurlURLs", zap.Any("response", r)) + return r, nil } diff --git a/protocol/messenger_linkpreview_test.go b/protocol/messenger_linkpreview_test.go index e8490245bd0..13f38094560 100644 --- a/protocol/messenger_linkpreview_test.go +++ b/protocol/messenger_linkpreview_test.go @@ -2,10 +2,9 @@ package protocol import ( "bytes" - "context" "fmt" "github.com/status-im/status-go/eth-node/crypto" - "github.com/status-im/status-go/protocol/requests" + "github.com/status-im/status-go/images" "io/ioutil" "math" "net/http" @@ -373,7 +372,7 @@ func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_Image() { s.assertContainsLongString(expected.Thumbnail.DataURI, preview.Thumbnail.DataURI, 100) } -func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContact() { +func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContactAdded() { identity, err := crypto.GenerateKey() s.Require().NoError(err) @@ -381,17 +380,30 @@ func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContact() { s.Require().NoError(err) s.Require().NotNil(c) - c.DisplayName = "TestDisplayName" - response, err := s.m.AddContact(context.Background(), &requests.AddContact{ID: c.ID, DisplayName: c.DisplayName}) + pubkey, err := c.PublicKey() + s.Require().NoError(err) + + shortKey, err := s.m.SerializePublicKey(crypto.CompressPubkey(pubkey)) s.Require().NoError(err) - s.Require().NotNil(response) - s.Require().Len(response.Contacts, 1) - //u, err := s.m.ShareUserURLWithChatKey(contact.ID) + payload, err := images.GetPayloadFromURI("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAiklEQVR4nOzWwQmFQAwG4ffEXmzLIizDImzLarQBhSwSGH7mO+9hh0DI9AthCI0hNIbQGEJjCI0hNIbQxITM1YfHfl69X3m2bsu/8i5mIobQGEJjCI0hNIbQlG+tUW83UtfNFjMRQ2gMofm8tUa3U9c2i5mIITSGqEnMRAyhMYTGEBpDaO4AAAD//5POEGncqtj1AAAAAElFTkSuQmCC") + s.Require().NoError(err) + + icon := images.IdentityImage{ + Width: 50, + Height: 50, + Payload: payload, + } + + c.Bio = "TestBio" + c.DisplayName = "TestDisplayName" + c.Images = map[string]images.IdentityImage{} + c.Images[images.SmallDimName] = icon + s.m.allContacts.Store(c.ID, c) + u, err := s.m.ShareUserURLWithData(c.ID) s.Require().NoError(err) - //stubbedClient := http.Client{Transport: &StubTransport{}} r, err := s.m.UnfurlURLs(nil, []string{u}) s.Require().NoError(err) s.Require().Len(r.StatusLinkPreviews, 1) @@ -399,9 +411,39 @@ func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContact() { preview := r.StatusLinkPreviews[0] s.Require().Equal(u, preview.URL) + s.Require().Nil(preview.Community) + s.Require().Nil(preview.Channel) s.Require().NotNil(preview.Contact) - s.Require().Equal(preview.Contact.DisplayName, c.DisplayName) - s.Require().Equal(preview.Contact.Description, "") + s.Require().Equal(shortKey, preview.Contact.PublicKey) + s.Require().Equal(c.DisplayName, preview.Contact.DisplayName) + s.Require().Equal(c.Bio, preview.Contact.Description) + s.Require().Equal(icon.Width, preview.Contact.Icon.Width) + s.Require().Equal(icon.Height, preview.Contact.Icon.Height) + s.Require().Equal("", preview.Contact.Icon.URL) + + expectedDataURI, err := images.GetPayloadDataURI(icon.Payload) + s.Require().NoError(err) + s.Require().Equal(expectedDataURI, preview.Contact.Icon.DataURI) +} + +func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContactUnknown() { + const u = "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" + + r, err := s.m.UnfurlURLs(nil, []string{u}) + s.Require().NoError(err) + s.Require().Len(r.StatusLinkPreviews, 1) + s.Require().Len(r.LinkPreviews, 0) + + preview := r.StatusLinkPreviews[0] + s.Require().Equal(u, preview.URL) s.Require().Nil(preview.Community) s.Require().Nil(preview.Channel) + s.Require().NotNil(preview.Contact) + s.Require().Equal("zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj", preview.Contact.PublicKey) + s.Require().Equal("Mark Cole", preview.Contact.DisplayName) + s.Require().Equal("Visual designer @Status, cat lover, pizza enthusiast, yoga afficionada", preview.Contact.Description) + s.Require().Equal(0, preview.Contact.Icon.Width) + s.Require().Equal(0, preview.Contact.Icon.Height) + s.Require().Equal("", preview.Contact.Icon.URL) + s.Require().Equal("", preview.Contact.Icon.DataURI) } diff --git a/protocol/messenger_share_urls.go b/protocol/messenger_share_urls.go index b69e1c7099f..39b00b2eb5d 100644 --- a/protocol/messenger_share_urls.go +++ b/protocol/messenger_share_urls.go @@ -464,6 +464,7 @@ func (m *Messenger) prepareEncodedUserData(contact *Contact) (string, string, er userProto := &protobuf.User{ DisplayName: contact.DisplayName, + Description: contact.Bio, } userData, err := proto.Marshal(userProto) diff --git a/protocol/messenger_share_urls_test.go b/protocol/messenger_share_urls_test.go index 76b9d19a58c..cb904ce8f30 100644 --- a/protocol/messenger_share_urls_test.go +++ b/protocol/messenger_share_urls_test.go @@ -362,7 +362,7 @@ func (s *MessengerShareUrlsSuite) TestParseUserURLWithChatKey() { s.Require().NotNil(urlData.Contact) s.Require().Equal(contact.DisplayName, urlData.Contact.DisplayName) - s.Require().Equal(contact.Bio, urlData.Contact.DisplayName) + s.Require().Equal(contact.Bio, urlData.Contact.Description) } func (s *MessengerShareUrlsSuite) TestShareUserURLWithENS() { diff --git a/protocol/messenger_test.go b/protocol/messenger_test.go index 4aa8692eab1..27f05ae3d8e 100644 --- a/protocol/messenger_test.go +++ b/protocol/messenger_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "github.com/golang/protobuf/proto" "io/ioutil" "math/big" "os" @@ -2285,6 +2286,13 @@ func (s *MessengerSuite) TestShouldResendEmoji() { s.True(ok) } +func (s *MessengerSuite) TestMarshalUnfurledStatusLink() { + const marshalled = "[{\"url\":\"https://status.app/u/TestUrl\",\"Payload\":{\"Contact\":{\"public_key\":\"TestPublicKey\",\"display_name\":\"TestDisplayName\",\"description\":\"Test description\",\"icon\":{\"payload\":\"iVBORw0KGgoAAAANSUg=\",\"width\":100,\"height\":200}}}}]" + var unmarshalled protobuf.UnfurledStatusLinks + err := proto.Unmarshal([]byte(marshalled), &unmarshalled) + s.Require().NoError(err) +} + func (s *MessengerSuite) TestSendMessageWithPreviews() { httpServer, err := server.NewMediaServer(s.m.database, nil, nil) s.Require().NoError(err) @@ -2310,7 +2318,7 @@ func (s *MessengerSuite) TestSendMessageWithPreviews() { } inputMsg.LinkPreviews = []common.LinkPreview{preview} - statusPreview := common.StatusLinkPreview{ + inputStatusPreview := common.StatusLinkPreview{ URL: "https://status.app/u/TestUrl", Contact: &common.StatusContactLinkPreview{ PublicKey: "TestPublicKey", @@ -2323,7 +2331,7 @@ func (s *MessengerSuite) TestSendMessageWithPreviews() { }, }, } - inputMsg.StatusLinkPreviews = []common.StatusLinkPreview{statusPreview} + inputMsg.StatusLinkPreviews = []common.StatusLinkPreview{inputStatusPreview} _, err = s.m.SendChatMessage(context.Background(), inputMsg) s.NoError(err) @@ -2335,14 +2343,14 @@ func (s *MessengerSuite) TestSendMessageWithPreviews() { // Test unfurled links have been saved. s.Require().Len(savedMsg.UnfurledLinks, 1) - unfurledLink := savedMsg.UnfurledLinks[0] - s.Require().Equal(preview.Type, unfurledLink.Type) - s.Require().Equal(preview.URL, unfurledLink.Url) - s.Require().Equal(preview.Title, unfurledLink.Title) - s.Require().Equal(preview.Description, unfurledLink.Description) + savedLinkProto := savedMsg.UnfurledLinks[0] + s.Require().Equal(preview.Type, savedLinkProto.Type) + s.Require().Equal(preview.URL, savedLinkProto.Url) + s.Require().Equal(preview.Title, savedLinkProto.Title) + s.Require().Equal(preview.Description, savedLinkProto.Description) // Test the saved link thumbnail can be encoded as a data URI. - expectedDataURI, err := images.GetPayloadDataURI(unfurledLink.ThumbnailPayload) + expectedDataURI, err := images.GetPayloadDataURI(savedLinkProto.ThumbnailPayload) s.Require().NoError(err) s.Require().Equal(preview.Thumbnail.DataURI, expectedDataURI) @@ -2351,12 +2359,42 @@ func (s *MessengerSuite) TestSendMessageWithPreviews() { savedMsg.LinkPreviews[0].Thumbnail.URL, ) - // Test unfurled status links have been saved + // Check saved message protobuf fields + s.Require().NotNil(savedMsg.UnfurledStatusLinks) + s.Require().Len(savedMsg.UnfurledStatusLinks.UnfurledStatusLinks, 1) + savedStatusLinkProto := savedMsg.UnfurledStatusLinks.UnfurledStatusLinks[0] + s.Require().Equal(inputStatusPreview.URL, savedStatusLinkProto.Url) + s.Require().NotNil(savedStatusLinkProto.GetContact()) + s.Require().Nil(savedStatusLinkProto.GetCommunity()) + s.Require().Nil(savedStatusLinkProto.GetChannel()) + + savedContactProto := savedStatusLinkProto.GetContact() + s.Require().Equal(inputStatusPreview.Contact.PublicKey, savedContactProto.PublicKey) + s.Require().Equal(inputStatusPreview.Contact.DisplayName, savedContactProto.DisplayName) + s.Require().Equal(inputStatusPreview.Contact.Description, savedContactProto.Description) + s.Require().NotNil(savedContactProto.Icon) + s.Require().Equal(inputStatusPreview.Contact.Icon.Width, int(savedContactProto.Icon.Width)) + s.Require().Equal(inputStatusPreview.Contact.Icon.Height, int(savedContactProto.Icon.Height)) + + iconDataURI, err := images.GetPayloadDataURI(savedContactProto.Icon.Payload) + s.Require().NoError(err) + s.Require().Equal(inputStatusPreview.Contact.Icon.DataURI, iconDataURI) + + // Check message `StatusLinkPreviews` properties s.Require().Len(savedMsg.StatusLinkPreviews, 1) - unfurledStatusLink := savedMsg.StatusLinkPreviews[0] - s.Require().Equal(statusPreview.URL, unfurledStatusLink.URL) - s.Require().NotNil(statusPreview.Contact) - s.Require().Equal(statusPreview.Contact.PublicKey, unfurledStatusLink.Contact.PublicKey) + savedStatusLinkPreview := savedMsg.StatusLinkPreviews[0] + s.Require().Equal(inputStatusPreview.URL, savedStatusLinkPreview.URL) + s.Require().NotNil(savedStatusLinkPreview.Contact) + + savedContact := savedStatusLinkPreview.Contact + s.Require().Equal(inputStatusPreview.Contact.PublicKey, savedContact.PublicKey) + s.Require().Equal(inputStatusPreview.Contact.DisplayName, savedContact.DisplayName) + s.Require().Equal(inputStatusPreview.Contact.Description, savedContact.Description) + s.Require().NotNil(savedContact.Icon) + s.Require().Equal(inputStatusPreview.Contact.Icon.Width, savedContact.Icon.Width) + s.Require().Equal(inputStatusPreview.Contact.Icon.Height, savedContact.Icon.Height) + expectedIconUrl := httpServer.MakeStatusLinkPreviewThumbnailURL(inputMsg.ID, inputStatusPreview.URL, "contact-icon") + s.Require().Equal(expectedIconUrl, savedContact.Icon.URL) } func (s *MessengerSuite) TestMessageSent() { diff --git a/protocol/protobuf/chat_message.proto b/protocol/protobuf/chat_message.proto index 6ee223388c0..09ba91f7d72 100644 --- a/protocol/protobuf/chat_message.proto +++ b/protocol/protobuf/chat_message.proto @@ -47,7 +47,7 @@ message EditMessage { ChatMessage.ContentType content_type = 7; repeated UnfurledLink unfurled_links = 8; - repeated UnfurledStatusLink unfurled_status_links = 9; + UnfurledStatusLinks unfurled_status_links = 9; } message DeleteMessage { @@ -170,6 +170,11 @@ message UnfurledStatusLink { } } +// Create a wrapper around repeated property for proper unmarshalling +message UnfurledStatusLinks { + repeated UnfurledStatusLink unfurled_status_links = 1; +} + message ChatMessage { // Lamport timestamp of the chat message uint64 clock = 1; @@ -212,7 +217,7 @@ message ChatMessage { repeated UnfurledLink unfurled_links = 16; - repeated UnfurledStatusLink unfurled_status_links = 17; + UnfurledStatusLinks unfurled_status_links = 17; enum ContentType { UNKNOWN_CONTENT_TYPE = 0; diff --git a/server/handlers.go b/server/handlers.go index dca0a598c8b..73aba0e60ec 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -15,6 +15,7 @@ import ( "strconv" "time" + "github.com/golang/protobuf/proto" "go.uber.org/zap" "github.com/status-im/status-go/images" @@ -33,11 +34,7 @@ const ( discordAuthorsPath = "/discord/authors" discordAttachmentsPath = basePath + "/discord/attachments" LinkPreviewThumbnailPath = "/link-preview/thumbnail" - StatusLinkPreviewThumbnailPath = "/status-link-preview/contact/icon" - //StatusLinkPreviewThumbnailPath = "/status-link-preview/community/icon" - //StatusLinkPreviewThumbnailPath = "/status-link-preview/community/banner" - //StatusLinkPreviewThumbnailPath = "/status-link-preview/channel/community-icon" - //StatusLinkPreviewThumbnailPath = "/status-link-preview/channel/community-banner" + StatusLinkPreviewThumbnailPath = "/status-link-preview/thumbnail" // Handler routes for pairing accountImagesPath = "/accountImages" @@ -46,6 +43,14 @@ const ( generateQRCode = "/GenerateQRCode" ) +const ( + ContactIcon = "contact-icon" + CommunityIcon = "community-icon" + CommunityBanner = "community-banner" + ChannelCommunityIcon = "channel-community-icon" + ChannelCommunityBanner = "channel-community-banner" +) + type HandlerPatternMap map[string]http.HandlerFunc func handleRequestDBMissing(logger *zap.Logger) http.HandlerFunc { @@ -85,6 +90,7 @@ type ImageParams struct { URL string MessageID string AttachmentID string + ImageID string Hash string Download bool @@ -271,6 +277,10 @@ func ParseImageParams(logger *zap.Logger, params url.Values) ImageParams { parsed.AuthorID = authorIds[0] } + if imageIds := params["image-id"]; len(imageIds) != 0 { + parsed.ImageID = imageIds[0] + } + urls := params["url"] if len(urls) != 0 { parsed.URL = urls[0] @@ -985,3 +995,124 @@ func handleLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc } } } + +func getStatusLinkThumbnailPayload(db *sql.DB, logger *zap.Logger, msgID string, URL string, imageId string) ([]byte, error) { + + var result []byte + err := db.QueryRow(`SELECT unfurled_status_links FROM user_messages WHERE id = ?`, msgID).Scan(&result) + if err != nil { + return nil, fmt.Errorf("could not find message with message-id '%s': %w", msgID, err) + } + + var links protobuf.UnfurledStatusLinks + err = proto.Unmarshal(result, &links) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal protobuf.UrlPreview: %w", err) + } + + for _, p := range links.UnfurledStatusLinks { + if p.Url != URL { + continue + } + + switch imageId { + case ContactIcon: + contact := p.GetContact() + if contact == nil { + return nil, fmt.Errorf("this is not a contact link") + } + if contact.Icon == nil { + return nil, fmt.Errorf("contact icon is empty") + } + return contact.Icon.Payload, nil + case CommunityIcon: + community := p.GetCommunity() + if community == nil { + return nil, fmt.Errorf("this is not a community link") + } + if community.Icon == nil { + return nil, fmt.Errorf("community icon is empty") + } + return community.Icon.Payload, nil + + case CommunityBanner: + community := p.GetCommunity() + if community == nil { + return nil, fmt.Errorf("this is not a community link") + } + if community.Banner == nil { + return nil, fmt.Errorf("community banner is empty") + } + return community.Banner.Payload, nil + + case ChannelCommunityIcon: + channel := p.GetChannel() + if channel == nil { + return nil, fmt.Errorf("this is not a channel link") + } + if channel.CommunityIcon == nil { + return nil, fmt.Errorf("channel community icon is empty") + } + return channel.CommunityIcon.Payload, nil + + case ChannelCommunityBanner: + channel := p.GetChannel() + if channel == nil { + return nil, fmt.Errorf("this is not a channel link") + } + if channel.CommunityBanner == nil { + return nil, fmt.Errorf("channel community banner is empty") + } + return channel.CommunityBanner.Payload, nil + } + } + + return nil, fmt.Errorf("invalid query parameter 'image-id' value") +} + +func handleStatusLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc { + + //StatusLinkPreviewImageURLs + //StatusLinkPreviewImageID + + return func(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + parsed := ParseImageParams(logger, params) + + if parsed.MessageID == "" { + http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest) + return + } + + if parsed.URL == "" { + http.Error(w, "missing query parameter 'url'", http.StatusBadRequest) + return + } + + if parsed.ImageID == "" { + http.Error(w, "missing query parameter 'image-id'", http.StatusBadRequest) + return + } + + thumbnail, err := getStatusLinkThumbnailPayload(db, logger, parsed.MessageID, parsed.URL, parsed.ImageID) + if err != nil { + logger.Error("failed to get thumbnail", zap.String("msgID", parsed.MessageID)) + http.Error(w, "failed to get thumbnail", http.StatusInternalServerError) + return + } + + mimeType, err := images.GetMimeType(thumbnail) + if err != nil { + http.Error(w, "mime type not supported", http.StatusNotImplemented) + return + } + + w.Header().Set("Content-Type", "image/"+mimeType) + w.Header().Set("Cache-Control", "no-store") + + _, err = w.Write(thumbnail) + if err != nil { + logger.Error("failed to write response", zap.Error(err)) + } + } +} diff --git a/server/server_media.go b/server/server_media.go index 4b8cd7eee43..c8023e6a904 100644 --- a/server/server_media.go +++ b/server/server_media.go @@ -47,7 +47,7 @@ func NewMediaServer(db *sql.DB, downloader *ipfs.Downloader, multiaccountsDB *mu imagesPath: handleImage(s.db, s.logger), ipfsPath: handleIPFS(s.downloader, s.logger), LinkPreviewThumbnailPath: handleLinkPreviewThumbnail(s.db, s.logger), - StatusLinkPreviewThumbnailPath: handleLinkPreviewThumbnail(s.db, s.logger), // FIXME: Use a separate function + StatusLinkPreviewThumbnailPath: handleStatusLinkPreviewThumbnail(s.db, s.logger), }) return s, nil @@ -74,10 +74,10 @@ func (s *MediaServer) MakeLinkPreviewThumbnailURL(msgID string, previewURL strin return u.String() } -func (s *MediaServer) MakeStatusLinkPreviewThumbnailURL(msgID string, previewURL string) string { +func (s *MediaServer) MakeStatusLinkPreviewThumbnailURL(msgID string, previewURL string, imageID string) string { u := s.MakeBaseURL() u.Path = StatusLinkPreviewThumbnailPath - u.RawQuery = url.Values{"message-id": {msgID}, "url": {previewURL}}.Encode() + u.RawQuery = url.Values{"message-id": {msgID}, "url": {previewURL}, "image-id": {string(imageID)}}.Encode() return u.String() } diff --git a/server/server_test.go b/server/server_test.go index 1a43b2bd294..4a5a7b54393 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -116,6 +116,16 @@ func (s *ServerURLSuite) TestServer_MakeLinkPreviewThumbnailURL() { s.serverNoPort.MakeLinkPreviewThumbnailURL("99", "https://github.com")) } +func (s *ServerURLSuite) TestServer_MakeStatusLinkPreviewThumbnailURL() { + s.Require().Equal( + baseURLWithCustomPort+"/status-link-preview/thumbnail?message-id=99&url=https%3A%2F%2Fstatus.app", + s.server.MakeStatusLinkPreviewThumbnailURL("99", "https://status.app")) + + s.testNoPort( + baseURLWithDefaultPort+"/status-link-preview/thumbnail?message-id=99&url=https%3A%2F%2Fstatus.app", + s.serverNoPort.MakeStatusLinkPreviewThumbnailURL("99", "https://status.app")) +} + func (s *ServerURLSuite) TestServer_MakeAudioURL() { s.Require().Equal( baseURLWithCustomPort+"/messages/audio?messageId=0xde1e7ebee71e",