2023-10-13 12:25:34 +00:00
|
|
|
package protocol
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
"github.com/status-im/status-go/api/multiformat"
|
|
|
|
"github.com/status-im/status-go/images"
|
|
|
|
"github.com/status-im/status-go/protocol/common"
|
2023-11-15 15:58:15 +00:00
|
|
|
"github.com/status-im/status-go/protocol/common/shard"
|
2023-10-13 12:25:34 +00:00
|
|
|
"github.com/status-im/status-go/protocol/communities"
|
|
|
|
)
|
|
|
|
|
|
|
|
type StatusUnfurler struct {
|
|
|
|
m *Messenger
|
|
|
|
logger *zap.Logger
|
|
|
|
url string
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewStatusUnfurler(URL string, messenger *Messenger, logger *zap.Logger) *StatusUnfurler {
|
|
|
|
return &StatusUnfurler{
|
|
|
|
m: messenger,
|
2023-10-24 10:15:32 +00:00
|
|
|
logger: logger.With(zap.String("url", URL)),
|
2023-10-13 12:25:34 +00:00
|
|
|
url: URL,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateThumbnail(image *images.IdentityImage, thumbnail *common.LinkPreviewThumbnail) error {
|
|
|
|
if image.IsEmpty() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-10-24 10:15:32 +00:00
|
|
|
width, height, err := images.GetImageDimensions(image.Payload)
|
2023-10-13 12:25:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to get image dimensions: %w", err)
|
|
|
|
}
|
|
|
|
|
2023-10-24 10:15:32 +00:00
|
|
|
dataURI, err := image.GetDataURI()
|
2023-10-13 12:25:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to get data uri: %w", err)
|
|
|
|
}
|
|
|
|
|
2023-10-24 10:15:32 +00:00
|
|
|
thumbnail.Width = width
|
|
|
|
thumbnail.Height = height
|
|
|
|
thumbnail.DataURI = dataURI
|
|
|
|
|
2023-10-13 12:25:34 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *StatusUnfurler) buildContactData(publicKey string) (*common.StatusContactLinkPreview, error) {
|
2023-11-09 16:24:35 +00:00
|
|
|
// contactID == "0x" + secp251k1 compressed public key as hex-encoded string
|
2023-10-13 12:25:34 +00:00
|
|
|
contactID, err := multiformat.DeserializeCompressedKey(publicKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
contact := u.m.GetContactByID(contactID)
|
|
|
|
|
|
|
|
// If no contact found locally, fetch it from waku
|
|
|
|
if contact == nil {
|
|
|
|
if contact, err = u.m.RequestContactInfoFromMailserver(contactID, true); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to request contact info from mailserver for public key '%s': %w", publicKey, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-09 16:24:35 +00:00
|
|
|
c := &common.StatusContactLinkPreview{
|
|
|
|
PublicKey: contactID,
|
|
|
|
DisplayName: contact.DisplayName,
|
|
|
|
Description: contact.Bio,
|
|
|
|
}
|
2023-10-13 12:25:34 +00:00
|
|
|
|
|
|
|
if image, ok := contact.Images[images.SmallDimName]; ok {
|
|
|
|
if err = updateThumbnail(&image, &c.Icon); err != nil {
|
2023-10-24 10:15:32 +00:00
|
|
|
u.logger.Warn("unfurling status link: failed to set contact thumbnail", zap.Error(err))
|
2023-10-13 12:25:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *StatusUnfurler) fillCommunityImages(community *communities.Community, icon *common.LinkPreviewThumbnail, banner *common.LinkPreviewThumbnail) error {
|
|
|
|
if image, ok := community.Images()[images.SmallDimName]; ok {
|
|
|
|
if err := updateThumbnail(&images.IdentityImage{Payload: image.Payload}, icon); err != nil {
|
|
|
|
u.logger.Warn("unfurling status link: failed to set community thumbnail", zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if image, ok := community.Images()[images.BannerIdentityName]; ok {
|
|
|
|
if err := updateThumbnail(&images.IdentityImage{Payload: image.Payload}, banner); err != nil {
|
|
|
|
u.logger.Warn("unfurling status link: failed to set community banner", zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-11-15 15:58:15 +00:00
|
|
|
func (u *StatusUnfurler) buildCommunityData(communityID string, shard *shard.Shard) (*communities.Community, *common.StatusCommunityLinkPreview, error) {
|
2023-10-13 12:25:34 +00:00
|
|
|
// This automatically checks the database
|
2023-11-03 10:30:24 +00:00
|
|
|
community, err := u.m.FetchCommunity(&FetchCommunityRequest{
|
|
|
|
CommunityKey: communityID,
|
|
|
|
Shard: shard,
|
|
|
|
TryDatabase: true,
|
|
|
|
WaitForResponse: true,
|
|
|
|
})
|
2023-10-13 12:25:34 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to get community info for communityID '%s': %w", communityID, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if community == nil {
|
|
|
|
return community, nil, fmt.Errorf("community info fetched, but it is empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
c := &common.StatusCommunityLinkPreview{
|
|
|
|
CommunityID: community.IDString(),
|
|
|
|
DisplayName: community.Name(),
|
|
|
|
Description: community.DescriptionText(),
|
|
|
|
MembersCount: uint32(community.MembersCount()),
|
|
|
|
Color: community.Color(),
|
|
|
|
}
|
|
|
|
|
|
|
|
err = u.fillCommunityImages(community, &c.Icon, &c.Banner)
|
|
|
|
if err != nil {
|
|
|
|
return community, c, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return community, c, nil
|
|
|
|
}
|
|
|
|
|
2023-11-15 15:58:15 +00:00
|
|
|
func (u *StatusUnfurler) buildChannelData(channelUUID string, communityID string, communityShard *shard.Shard) (*common.StatusCommunityChannelLinkPreview, error) {
|
2023-11-03 10:30:24 +00:00
|
|
|
community, communityData, err := u.buildCommunityData(communityID, communityShard)
|
2023-10-13 12:25:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to build channel community data: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
channel, ok := community.Chats()[channelUUID]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("channel with channelID '%s' not found in community '%s'", channelUUID, communityID)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &common.StatusCommunityChannelLinkPreview{
|
|
|
|
ChannelUUID: channelUUID,
|
|
|
|
Emoji: channel.Identity.Emoji,
|
|
|
|
DisplayName: channel.Identity.DisplayName,
|
|
|
|
Description: channel.Identity.Description,
|
|
|
|
Color: channel.Identity.Color,
|
|
|
|
Community: communityData,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *StatusUnfurler) Unfurl() (*common.StatusLinkPreview, error) {
|
|
|
|
preview := new(common.StatusLinkPreview)
|
|
|
|
preview.URL = u.url
|
|
|
|
|
2023-11-10 16:33:37 +00:00
|
|
|
resp, err := ParseSharedURL(u.url)
|
2023-10-13 12:25:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse shared url: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a URL has been successfully parsed,
|
|
|
|
// any further errors should not be returned, only logged.
|
|
|
|
|
|
|
|
if resp.Contact != nil {
|
|
|
|
preview.Contact, err = u.buildContactData(resp.Contact.PublicKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error when building contact data: %w", err)
|
|
|
|
}
|
|
|
|
return preview, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Currently channel data comes together with community data,
|
|
|
|
// both `Community` and `Channel` fields will be present.
|
|
|
|
// So we check for Channel first, then Community.
|
|
|
|
|
|
|
|
if resp.Channel != nil {
|
|
|
|
if resp.Community == nil {
|
|
|
|
return preview, fmt.Errorf("channel community can't be empty")
|
|
|
|
}
|
2023-11-03 10:30:24 +00:00
|
|
|
preview.Channel, err = u.buildChannelData(resp.Channel.ChannelUUID, resp.Community.CommunityID, resp.Shard)
|
2023-10-13 12:25:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error when building channel data: %w", err)
|
|
|
|
}
|
|
|
|
return preview, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.Community != nil {
|
2023-11-03 10:30:24 +00:00
|
|
|
_, preview.Community, err = u.buildCommunityData(resp.Community.CommunityID, resp.Shard)
|
2023-10-13 12:25:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error when building community data: %w", err)
|
|
|
|
}
|
|
|
|
return preview, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("shared url does not contain contact, community or channel data")
|
|
|
|
}
|