status-go/protocol/messenger_share_urls.go

703 lines
18 KiB
Go
Raw Permalink Normal View History

package protocol
import (
"bytes"
"encoding/base64"
"fmt"
"regexp"
"strings"
"github.com/golang/protobuf/proto"
"github.com/andybalholm/brotli"
"github.com/status-im/status-go/api/multiformat"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"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"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
2023-10-25 16:49:18 +00:00
"github.com/status-im/status-go/services/utils"
)
type CommunityURLData struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
MembersCount uint32 `json:"membersCount"`
Color string `json:"color"`
TagIndices []uint32 `json:"tagIndices"`
CommunityID string `json:"communityId"`
}
type CommunityChannelURLData struct {
Emoji string `json:"emoji"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Color string `json:"color"`
ChannelUUID string `json:"channelUuid"`
}
type ContactURLData struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
PublicKey string `json:"publicKey"`
}
type TransactionURLData struct {
TxType int `json:"txType"`
Address string `json:"address"`
Amount string `json:"amount"`
Asset string `json:"asset"`
ChainID int `json:"chainId"`
ToAsset string `json:"toAsset"`
}
type URLDataResponse struct {
Community *CommunityURLData `json:"community"`
Channel *CommunityChannelURLData `json:"channel"`
Contact *ContactURLData `json:"contact"`
Transaction *TransactionURLData `json:"tx"`
Shard *shard.Shard `json:"shard,omitempty"`
}
const baseShareURL = "https://status.app"
const userPath = "u#"
const userWithDataPath = "u/"
const communityPath = "c#"
const communityWithDataPath = "c/"
const channelPath = "cc/"
const transactionPath = "tx/"
const sharedURLUserPrefix = baseShareURL + "/" + userPath
const sharedURLUserPrefixWithData = baseShareURL + "/" + userWithDataPath
const sharedURLCommunityPrefix = baseShareURL + "/" + communityPath
const sharedURLCommunityPrefixWithData = baseShareURL + "/" + communityWithDataPath
const sharedURLChannelPrefixWithData = baseShareURL + "/" + channelPath
const sharedURLTransaction = baseShareURL + "/" + transactionPath
const channelUUIDRegExp = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$"
var channelRegExp = regexp.MustCompile(channelUUIDRegExp)
func decodeCommunityID(serialisedPublicKey string) (string, error) {
deserializedCommunityID, err := multiformat.DeserializeCompressedKey(serialisedPublicKey)
if err != nil {
return "", err
}
communityID, err := common.HexToPubkey(deserializedCommunityID)
if err != nil {
return "", err
}
return types.EncodeHex(crypto.CompressPubkey(communityID)), nil
}
func serializePublicKey(compressedKey types.HexBytes) (string, error) {
return utils.SerializePublicKey(compressedKey)
}
func deserializePublicKey(compressedKey string) (types.HexBytes, error) {
2023-10-25 16:49:18 +00:00
return utils.DeserializePublicKey(compressedKey)
}
func (m *Messenger) ShareCommunityURLWithChatKey(communityID types.HexBytes) (string, error) {
shortKey, err := serializePublicKey(communityID)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/c#%s", baseShareURL, shortKey), nil
}
func parseCommunityURLWithChatKey(urlData string) (*URLDataResponse, error) {
communityID, err := decodeCommunityID(urlData)
if err != nil {
return nil, err
}
return &URLDataResponse{
Community: &CommunityURLData{
CommunityID: communityID,
TagIndices: []uint32{},
},
Shard: nil,
}, nil
}
func (m *Messenger) prepareEncodedCommunityData(community *communities.Community) (string, string, error) {
communityProto := &protobuf.Community{
DisplayName: community.Identity().DisplayName,
Description: community.DescriptionText(),
MembersCount: uint32(community.MembersCount()),
Color: community.Identity().GetColor(),
TagIndices: community.TagsIndices(),
}
communityData, err := proto.Marshal(communityProto)
if err != nil {
return "", "", err
}
urlDataProto := &protobuf.URLData{
Content: communityData,
Shard: community.Shard().Protobuffer(),
}
urlData, err := proto.Marshal(urlDataProto)
if err != nil {
return "", "", err
}
shortKey, err := serializePublicKey(community.ID())
if err != nil {
return "", "", err
}
encodedData, err := encodeDataURL(urlData)
if err != nil {
return "", "", err
}
return encodedData, shortKey, nil
}
func (m *Messenger) ShareCommunityURLWithData(communityID types.HexBytes) (string, error) {
community, err := m.GetCommunityByID(communityID)
if err != nil {
return "", err
}
data, shortKey, err := m.prepareEncodedCommunityData(community)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/c/%s#%s", baseShareURL, data, shortKey), nil
}
func parseCommunityURLWithData(data string, chatKey string) (*URLDataResponse, error) {
communityID, err := deserializePublicKey(chatKey)
if err != nil {
return nil, err
}
urlData, err := decodeDataURL(data)
if err != nil {
return nil, err
}
var urlDataProto protobuf.URLData
err = proto.Unmarshal(urlData, &urlDataProto)
if err != nil {
return nil, err
}
var communityProto protobuf.Community
err = proto.Unmarshal(urlDataProto.Content, &communityProto)
if err != nil {
return nil, err
}
var tagIndices []uint32
if communityProto.TagIndices != nil {
tagIndices = communityProto.TagIndices
} else {
tagIndices = []uint32{}
}
return &URLDataResponse{
2023-10-13 12:25:34 +00:00
Community: &CommunityURLData{
DisplayName: communityProto.DisplayName,
Description: communityProto.Description,
MembersCount: communityProto.MembersCount,
Color: communityProto.Color,
TagIndices: tagIndices,
CommunityID: types.EncodeHex(communityID),
},
2023-11-15 15:58:15 +00:00
Shard: shard.FromProtobuff(urlDataProto.Shard),
}, nil
}
func (m *Messenger) ShareCommunityChannelURLWithChatKey(request *requests.CommunityChannelShareURL) (string, error) {
if err := request.Validate(); err != nil {
return "", err
}
shortKey, err := serializePublicKey(request.CommunityID)
if err != nil {
return "", err
}
valid, err := regexp.MatchString(channelUUIDRegExp, request.ChannelID)
if err != nil {
return "", err
}
if !valid {
return "", fmt.Errorf("channelID should be UUID, got %s", request.ChannelID)
}
return fmt.Sprintf("%s/cc/%s#%s", baseShareURL, request.ChannelID, shortKey), nil
}
func parseCommunityChannelURLWithChatKey(channelID string, publicKey string) (*URLDataResponse, error) {
valid, err := regexp.MatchString(channelUUIDRegExp, channelID)
if err != nil {
return nil, err
}
if !valid {
return nil, fmt.Errorf("channelID should be UUID, got %s", channelID)
}
communityID, err := decodeCommunityID(publicKey)
if err != nil {
return nil, err
}
return &URLDataResponse{
Community: &CommunityURLData{
CommunityID: communityID,
TagIndices: []uint32{},
},
Channel: &CommunityChannelURLData{
ChannelUUID: channelID,
},
Shard: nil,
}, nil
}
func (m *Messenger) prepareEncodedCommunityChannelData(community *communities.Community, channel *protobuf.CommunityChat, channelID string) (string, string, error) {
communityProto := &protobuf.Community{
DisplayName: community.Identity().DisplayName,
Description: community.DescriptionText(),
MembersCount: uint32(community.MembersCount()),
Color: community.Identity().GetColor(),
TagIndices: community.TagsIndices(),
}
channelProto := &protobuf.Channel{
DisplayName: channel.Identity.DisplayName,
Description: channel.Identity.Description,
Emoji: channel.Identity.Emoji,
Color: channel.GetIdentity().Color,
Community: communityProto,
Uuid: channelID,
}
channelData, err := proto.Marshal(channelProto)
if err != nil {
return "", "", err
}
urlDataProto := &protobuf.URLData{
Content: channelData,
Shard: community.Shard().Protobuffer(),
}
urlData, err := proto.Marshal(urlDataProto)
if err != nil {
return "", "", err
}
shortKey, err := serializePublicKey(community.ID())
if err != nil {
return "", "", err
}
encodedData, err := encodeDataURL(urlData)
if err != nil {
return "", "", err
}
return encodedData, shortKey, nil
}
func (m *Messenger) ShareTransactionURL(request *requests.TransactionShareURL) (string, error) {
encodedData, err := m.prepareTransactionUrl(request)
if err != nil {
return "", err
}
return fmt.Sprintf("%s%s", sharedURLTransaction, encodedData), nil
}
func (m *Messenger) prepareTransactionUrl(request *requests.TransactionShareURL) (string, error) {
if err := request.Validate(); err != nil {
return "", err
}
txProto := &protobuf.Transaction{
TxType: uint32(request.TxType),
Address: request.Address,
Amount: request.Amount,
Asset: request.Asset,
ChainId: uint32(request.ChainID),
ToAsset: request.ToAsset,
}
txData, err := proto.Marshal(txProto)
if err != nil {
return "", err
}
urlDataProto := &protobuf.URLData{
Content: txData,
}
urlData, err := proto.Marshal(urlDataProto)
if err != nil {
return "", err
}
encodedData, err := encodeDataURL(urlData)
if err != nil {
return "", err
}
return encodedData, nil
}
func parseTransactionURL(data string) (*URLDataResponse, error) {
urlData, err := decodeDataURL(data)
if err != nil {
return nil, err
}
var urlDataProto protobuf.URLData
err = proto.Unmarshal(urlData, &urlDataProto)
if err != nil {
return nil, err
}
var txProto protobuf.Transaction
err = proto.Unmarshal(urlDataProto.Content, &txProto)
if err != nil {
return nil, err
}
return &URLDataResponse{
Transaction: &TransactionURLData{
TxType: int(txProto.TxType),
Address: txProto.Address,
Amount: txProto.Amount,
Asset: txProto.Asset,
ChainID: int(txProto.ChainId),
ToAsset: txProto.ToAsset,
},
}, nil
}
func (m *Messenger) ShareCommunityChannelURLWithData(request *requests.CommunityChannelShareURL) (string, error) {
if err := request.Validate(); err != nil {
return "", err
}
valid, err := regexp.MatchString(channelUUIDRegExp, request.ChannelID)
if err != nil {
return "", err
}
if !valid {
return "nil", fmt.Errorf("channelID should be UUID, got %s", request.ChannelID)
}
community, err := m.GetCommunityByID(request.CommunityID)
if err != nil {
return "", err
}
channel := community.Chats()[request.ChannelID]
if channel == nil {
return "", fmt.Errorf("channel with channelID %s not found", request.ChannelID)
}
data, shortKey, err := m.prepareEncodedCommunityChannelData(community, channel, request.ChannelID)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/cc/%s#%s", baseShareURL, data, shortKey), nil
}
func parseCommunityChannelURLWithData(data string, chatKey string) (*URLDataResponse, error) {
communityID, err := deserializePublicKey(chatKey)
if err != nil {
return nil, err
}
urlData, err := decodeDataURL(data)
if err != nil {
return nil, err
}
var urlDataProto protobuf.URLData
err = proto.Unmarshal(urlData, &urlDataProto)
if err != nil {
return nil, err
}
var channelProto protobuf.Channel
err = proto.Unmarshal(urlDataProto.Content, &channelProto)
if err != nil {
return nil, err
}
var tagIndices []uint32
if channelProto.Community.TagIndices != nil {
tagIndices = channelProto.Community.TagIndices
} else {
tagIndices = []uint32{}
}
return &URLDataResponse{
2023-10-13 12:25:34 +00:00
Community: &CommunityURLData{
DisplayName: channelProto.Community.DisplayName,
Description: channelProto.Community.Description,
MembersCount: channelProto.Community.MembersCount,
Color: channelProto.Community.Color,
TagIndices: tagIndices,
CommunityID: types.EncodeHex(communityID),
},
2023-10-13 12:25:34 +00:00
Channel: &CommunityChannelURLData{
Emoji: channelProto.Emoji,
DisplayName: channelProto.DisplayName,
Description: channelProto.Description,
Color: channelProto.Color,
ChannelUUID: channelProto.Uuid,
},
2023-11-15 15:58:15 +00:00
Shard: shard.FromProtobuff(urlDataProto.Shard),
}, nil
}
func (m *Messenger) ShareUserURLWithChatKey(contactID string) (string, error) {
publicKey, err := common.HexToPubkey(contactID)
if err != nil {
return "", err
}
shortKey, err := serializePublicKey(crypto.CompressPubkey(publicKey))
if err != nil {
return "", err
}
return fmt.Sprintf("%s/u#%s", baseShareURL, shortKey), nil
}
func parseUserURLWithChatKey(urlData string) (*URLDataResponse, error) {
pubKeyBytes, err := deserializePublicKey(urlData)
if err != nil {
return nil, err
}
pubKey, err := crypto.DecompressPubkey(pubKeyBytes)
if err != nil {
return nil, err
}
serializedPublicKey, err := multiformat.SerializeLegacyKey(common.PubkeyToHex(pubKey))
if err != nil {
return nil, err
}
return &URLDataResponse{
Contact: &ContactURLData{
PublicKey: serializedPublicKey,
},
}, nil
}
func (m *Messenger) ShareUserURLWithENS(contactID string) (string, error) {
contact := m.GetContactByID(contactID)
if contact == nil {
return "", ErrContactNotFound
}
return fmt.Sprintf("%s/u#%s", baseShareURL, contact.EnsName), nil
}
func parseUserURLWithENS(ensName string) (*URLDataResponse, error) {
// TODO: fetch contact by ens name
return nil, fmt.Errorf("not implemented yet")
}
func (m *Messenger) prepareEncodedUserData(contact *Contact) (string, string, error) {
pk, err := contact.PublicKey()
if err != nil {
return "", "", err
}
shortKey, err := serializePublicKey(crypto.CompressPubkey(pk))
if err != nil {
return "", "", err
}
userProto := &protobuf.User{
DisplayName: contact.DisplayName,
2023-10-13 12:25:34 +00:00
Description: contact.Bio,
}
userData, err := proto.Marshal(userProto)
if err != nil {
return "", "", err
}
urlDataProto := &protobuf.URLData{
Content: userData,
}
urlData, err := proto.Marshal(urlDataProto)
if err != nil {
return "", "", err
}
encodedData, err := encodeDataURL(urlData)
if err != nil {
return "", "", err
}
return encodedData, shortKey, nil
}
func (m *Messenger) ShareUserURLWithData(contactID string) (string, error) {
contact := m.GetContactByID(contactID)
if contact == nil {
return "", ErrContactNotFound
}
data, shortKey, err := m.prepareEncodedUserData(contact)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/u/%s#%s", baseShareURL, data, shortKey), nil
}
func parseUserURLWithData(data string, chatKey string) (*URLDataResponse, error) {
urlData, err := decodeDataURL(data)
if err != nil {
return nil, err
}
var urlDataProto protobuf.URLData
err = proto.Unmarshal(urlData, &urlDataProto)
if err != nil {
return nil, err
}
var userProto protobuf.User
err = proto.Unmarshal(urlDataProto.Content, &userProto)
if err != nil {
return nil, err
}
return &URLDataResponse{
2023-10-13 12:25:34 +00:00
Contact: &ContactURLData{
DisplayName: userProto.DisplayName,
Description: userProto.Description,
PublicKey: chatKey,
},
}, nil
}
func IsStatusSharedURL(url string) bool {
return strings.HasPrefix(url, sharedURLUserPrefix) ||
strings.HasPrefix(url, sharedURLUserPrefixWithData) ||
strings.HasPrefix(url, sharedURLCommunityPrefix) ||
strings.HasPrefix(url, sharedURLCommunityPrefixWithData) ||
strings.HasPrefix(url, sharedURLChannelPrefixWithData) ||
strings.HasPrefix(url, sharedURLTransaction)
2023-10-13 12:25:34 +00:00
}
func splitSharedURLData(data string) (string, string, error) {
const count = 2
contents := strings.SplitN(data, "#", count)
if len(contents) != count {
return "", "", fmt.Errorf("url should contain at least one `#` separator")
}
return contents[0], contents[1], nil
}
func ParseSharedURL(url string) (*URLDataResponse, error) {
if strings.HasPrefix(url, sharedURLUserPrefix) {
chatKey := strings.TrimPrefix(url, sharedURLUserPrefix)
if strings.HasPrefix(chatKey, "zQ3sh") {
return parseUserURLWithChatKey(chatKey)
}
return parseUserURLWithENS(chatKey)
}
if strings.HasPrefix(url, sharedURLUserPrefixWithData) {
trimmedURL := strings.TrimPrefix(url, sharedURLUserPrefixWithData)
encodedData, chatKey, err := splitSharedURLData(trimmedURL)
if err != nil {
return nil, err
}
return parseUserURLWithData(encodedData, chatKey)
}
if strings.HasPrefix(url, sharedURLCommunityPrefix) {
chatKey := strings.TrimPrefix(url, sharedURLCommunityPrefix)
return parseCommunityURLWithChatKey(chatKey)
}
if strings.HasPrefix(url, sharedURLCommunityPrefixWithData) {
trimmedURL := strings.TrimPrefix(url, sharedURLCommunityPrefixWithData)
encodedData, chatKey, err := splitSharedURLData(trimmedURL)
if err != nil {
return nil, err
}
return parseCommunityURLWithData(encodedData, chatKey)
}
if strings.HasPrefix(url, sharedURLChannelPrefixWithData) {
trimmedURL := strings.TrimPrefix(url, sharedURLChannelPrefixWithData)
encodedData, chatKey, err := splitSharedURLData(trimmedURL)
if err != nil {
return nil, err
}
if channelRegExp.MatchString(encodedData) {
return parseCommunityChannelURLWithChatKey(encodedData, chatKey)
}
return parseCommunityChannelURLWithData(encodedData, chatKey)
}
if strings.HasPrefix(url, sharedURLTransaction) {
trimmedURL := strings.TrimPrefix(url, sharedURLTransaction)
return parseTransactionURL(trimmedURL)
}
return nil, fmt.Errorf("not a status shared url")
}
func encodeDataURL(data []byte) (string, error) {
bb := bytes.NewBuffer([]byte{})
writer := brotli.NewWriter(bb)
_, err := writer.Write(data)
if err != nil {
return "", err
}
err = writer.Close()
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bb.Bytes()), nil
}
func decodeDataURL(data string) ([]byte, error) {
decoded, err := base64.URLEncoding.DecodeString(data)
if err != nil {
return nil, err
}
output := make([]byte, 4096)
bb := bytes.NewBuffer(decoded)
reader := brotli.NewReader(bb)
n, err := reader.Read(output)
if err != nil {
return nil, err
}
return output[:n], nil
}