status-im/status-go

View on GitHub
protocol/linkpreview_unfurler_status.go

Summary

Maintainability
A
0 mins
Test Coverage
F
53%
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"
    "github.com/status-im/status-go/protocol/common/shard"
    "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,
        logger: logger.With(zap.String("url", URL)),
        url:    URL,
    }
}

func updateThumbnail(image *images.IdentityImage, thumbnail *common.LinkPreviewThumbnail) error {
    if image.IsEmpty() {
        return nil
    }

    width, height, err := images.GetImageDimensions(image.Payload)
    if err != nil {
        return fmt.Errorf("failed to get image dimensions: %w", err)
    }

    dataURI, err := image.GetDataURI()
    if err != nil {
        return fmt.Errorf("failed to get data uri: %w", err)
    }

    thumbnail.Width = width
    thumbnail.Height = height
    thumbnail.DataURI = dataURI

    return nil
}

func (u *StatusUnfurler) buildContactData(publicKey string) (*common.StatusContactLinkPreview, error) {
    // contactID == "0x" + secp251k1 compressed public key as hex-encoded string
    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 {
        contact, err = u.m.FetchContact(contactID, true)
        if err != nil {
            return nil, fmt.Errorf("failed to request contact info from mailserver for public key '%s': %w", publicKey, err)
        }
        if contact == nil {
            return nil, fmt.Errorf("contact wasn't found at the store node %s", publicKey)
        }
    }

    c := &common.StatusContactLinkPreview{
        PublicKey:   contactID,
        DisplayName: contact.DisplayName,
        Description: contact.Bio,
    }

    if image, ok := contact.Images[images.SmallDimName]; ok {
        if err = updateThumbnail(&image, &c.Icon); err != nil {
            u.logger.Warn("unfurling status link: failed to set contact thumbnail", zap.Error(err))
        }
    }

    return c, nil
}

func (u *StatusUnfurler) buildCommunityData(communityID string, shard *shard.Shard) (*communities.Community, *common.StatusCommunityLinkPreview, error) {
    // This automatically checks the database
    community, err := u.m.FetchCommunity(&FetchCommunityRequest{
        CommunityKey:    communityID,
        Shard:           shard,
        TryDatabase:     true,
        WaitForResponse: true,
    })

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

    statusCommunityLinkPreviews, err := community.ToStatusLinkPreview()
    if err != nil {
        return nil, nil, fmt.Errorf("failed to get status community link preview for communityID '%s': %w", communityID, err)
    }

    return community, statusCommunityLinkPreviews, nil
}

func (u *StatusUnfurler) buildChannelData(channelUUID string, communityID string, communityShard *shard.Shard) (*common.StatusCommunityChannelLinkPreview, error) {
    community, communityData, err := u.buildCommunityData(communityID, communityShard)
    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

    resp, err := ParseSharedURL(u.url)
    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")
        }
        preview.Channel, err = u.buildChannelData(resp.Channel.ChannelUUID, resp.Community.CommunityID, resp.Shard)
        if err != nil {
            return nil, fmt.Errorf("error when building channel data: %w", err)
        }
        return preview, nil
    }

    if resp.Community != nil {
        _, preview.Community, err = u.buildCommunityData(resp.Community.CommunityID, resp.Shard)
        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")
}