github.com/status-im/status-go@v1.1.0/protocol/messenger_share_urls.go (about)

     1  package protocol
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/golang/protobuf/proto"
    11  
    12  	"github.com/andybalholm/brotli"
    13  
    14  	"github.com/status-im/status-go/api/multiformat"
    15  	"github.com/status-im/status-go/eth-node/crypto"
    16  	"github.com/status-im/status-go/eth-node/types"
    17  	"github.com/status-im/status-go/protocol/common"
    18  	"github.com/status-im/status-go/protocol/common/shard"
    19  	"github.com/status-im/status-go/protocol/communities"
    20  	"github.com/status-im/status-go/protocol/protobuf"
    21  	"github.com/status-im/status-go/protocol/requests"
    22  	"github.com/status-im/status-go/services/utils"
    23  )
    24  
    25  type CommunityURLData struct {
    26  	DisplayName  string   `json:"displayName"`
    27  	Description  string   `json:"description"`
    28  	MembersCount uint32   `json:"membersCount"`
    29  	Color        string   `json:"color"`
    30  	TagIndices   []uint32 `json:"tagIndices"`
    31  	CommunityID  string   `json:"communityId"`
    32  }
    33  
    34  type CommunityChannelURLData struct {
    35  	Emoji       string `json:"emoji"`
    36  	DisplayName string `json:"displayName"`
    37  	Description string `json:"description"`
    38  	Color       string `json:"color"`
    39  	ChannelUUID string `json:"channelUuid"`
    40  }
    41  
    42  type ContactURLData struct {
    43  	DisplayName string `json:"displayName"`
    44  	Description string `json:"description"`
    45  	PublicKey   string `json:"publicKey"`
    46  }
    47  
    48  type URLDataResponse struct {
    49  	Community *CommunityURLData        `json:"community"`
    50  	Channel   *CommunityChannelURLData `json:"channel"`
    51  	Contact   *ContactURLData          `json:"contact"`
    52  	Shard     *shard.Shard             `json:"shard,omitempty"`
    53  }
    54  
    55  const baseShareURL = "https://status.app"
    56  const userPath = "u#"
    57  const userWithDataPath = "u/"
    58  const communityPath = "c#"
    59  const communityWithDataPath = "c/"
    60  const channelPath = "cc/"
    61  
    62  const sharedURLUserPrefix = baseShareURL + "/" + userPath
    63  const sharedURLUserPrefixWithData = baseShareURL + "/" + userWithDataPath
    64  const sharedURLCommunityPrefix = baseShareURL + "/" + communityPath
    65  const sharedURLCommunityPrefixWithData = baseShareURL + "/" + communityWithDataPath
    66  const sharedURLChannelPrefixWithData = baseShareURL + "/" + channelPath
    67  
    68  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}$"
    69  
    70  var channelRegExp = regexp.MustCompile(channelUUIDRegExp)
    71  
    72  func decodeCommunityID(serialisedPublicKey string) (string, error) {
    73  	deserializedCommunityID, err := multiformat.DeserializeCompressedKey(serialisedPublicKey)
    74  	if err != nil {
    75  		return "", err
    76  	}
    77  
    78  	communityID, err := common.HexToPubkey(deserializedCommunityID)
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  
    83  	return types.EncodeHex(crypto.CompressPubkey(communityID)), nil
    84  }
    85  
    86  func serializePublicKey(compressedKey types.HexBytes) (string, error) {
    87  	return utils.SerializePublicKey(compressedKey)
    88  }
    89  
    90  func deserializePublicKey(compressedKey string) (types.HexBytes, error) {
    91  	return utils.DeserializePublicKey(compressedKey)
    92  }
    93  
    94  func (m *Messenger) ShareCommunityURLWithChatKey(communityID types.HexBytes) (string, error) {
    95  	shortKey, err := serializePublicKey(communityID)
    96  	if err != nil {
    97  		return "", err
    98  	}
    99  	return fmt.Sprintf("%s/c#%s", baseShareURL, shortKey), nil
   100  }
   101  
   102  func parseCommunityURLWithChatKey(urlData string) (*URLDataResponse, error) {
   103  	communityID, err := decodeCommunityID(urlData)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	return &URLDataResponse{
   109  		Community: &CommunityURLData{
   110  			CommunityID: communityID,
   111  			TagIndices:  []uint32{},
   112  		},
   113  		Shard: nil,
   114  	}, nil
   115  }
   116  
   117  func (m *Messenger) prepareEncodedCommunityData(community *communities.Community) (string, string, error) {
   118  	communityProto := &protobuf.Community{
   119  		DisplayName:  community.Identity().DisplayName,
   120  		Description:  community.DescriptionText(),
   121  		MembersCount: uint32(community.MembersCount()),
   122  		Color:        community.Identity().GetColor(),
   123  		TagIndices:   community.TagsIndices(),
   124  	}
   125  
   126  	communityData, err := proto.Marshal(communityProto)
   127  	if err != nil {
   128  		return "", "", err
   129  	}
   130  
   131  	urlDataProto := &protobuf.URLData{
   132  		Content: communityData,
   133  		Shard:   community.Shard().Protobuffer(),
   134  	}
   135  
   136  	urlData, err := proto.Marshal(urlDataProto)
   137  	if err != nil {
   138  		return "", "", err
   139  	}
   140  
   141  	shortKey, err := serializePublicKey(community.ID())
   142  	if err != nil {
   143  		return "", "", err
   144  	}
   145  
   146  	encodedData, err := encodeDataURL(urlData)
   147  	if err != nil {
   148  		return "", "", err
   149  	}
   150  
   151  	return encodedData, shortKey, nil
   152  }
   153  
   154  func (m *Messenger) ShareCommunityURLWithData(communityID types.HexBytes) (string, error) {
   155  	community, err := m.GetCommunityByID(communityID)
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  
   160  	data, shortKey, err := m.prepareEncodedCommunityData(community)
   161  	if err != nil {
   162  		return "", err
   163  	}
   164  
   165  	return fmt.Sprintf("%s/c/%s#%s", baseShareURL, data, shortKey), nil
   166  }
   167  
   168  func parseCommunityURLWithData(data string, chatKey string) (*URLDataResponse, error) {
   169  	communityID, err := deserializePublicKey(chatKey)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	urlData, err := decodeDataURL(data)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	var urlDataProto protobuf.URLData
   180  	err = proto.Unmarshal(urlData, &urlDataProto)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	var communityProto protobuf.Community
   186  	err = proto.Unmarshal(urlDataProto.Content, &communityProto)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	var tagIndices []uint32
   192  	if communityProto.TagIndices != nil {
   193  		tagIndices = communityProto.TagIndices
   194  	} else {
   195  		tagIndices = []uint32{}
   196  	}
   197  
   198  	return &URLDataResponse{
   199  		Community: &CommunityURLData{
   200  			DisplayName:  communityProto.DisplayName,
   201  			Description:  communityProto.Description,
   202  			MembersCount: communityProto.MembersCount,
   203  			Color:        communityProto.Color,
   204  			TagIndices:   tagIndices,
   205  			CommunityID:  types.EncodeHex(communityID),
   206  		},
   207  		Shard: shard.FromProtobuff(urlDataProto.Shard),
   208  	}, nil
   209  }
   210  
   211  func (m *Messenger) ShareCommunityChannelURLWithChatKey(request *requests.CommunityChannelShareURL) (string, error) {
   212  	if err := request.Validate(); err != nil {
   213  		return "", err
   214  	}
   215  
   216  	shortKey, err := serializePublicKey(request.CommunityID)
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  
   221  	valid, err := regexp.MatchString(channelUUIDRegExp, request.ChannelID)
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  
   226  	if !valid {
   227  		return "", fmt.Errorf("channelID should be UUID, got %s", request.ChannelID)
   228  	}
   229  
   230  	return fmt.Sprintf("%s/cc/%s#%s", baseShareURL, request.ChannelID, shortKey), nil
   231  }
   232  
   233  func parseCommunityChannelURLWithChatKey(channelID string, publicKey string) (*URLDataResponse, error) {
   234  	valid, err := regexp.MatchString(channelUUIDRegExp, channelID)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	if !valid {
   240  		return nil, fmt.Errorf("channelID should be UUID, got %s", channelID)
   241  	}
   242  
   243  	communityID, err := decodeCommunityID(publicKey)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	return &URLDataResponse{
   249  		Community: &CommunityURLData{
   250  			CommunityID: communityID,
   251  			TagIndices:  []uint32{},
   252  		},
   253  		Channel: &CommunityChannelURLData{
   254  			ChannelUUID: channelID,
   255  		},
   256  		Shard: nil,
   257  	}, nil
   258  }
   259  
   260  func (m *Messenger) prepareEncodedCommunityChannelData(community *communities.Community, channel *protobuf.CommunityChat, channelID string) (string, string, error) {
   261  	communityProto := &protobuf.Community{
   262  		DisplayName:  community.Identity().DisplayName,
   263  		Description:  community.DescriptionText(),
   264  		MembersCount: uint32(community.MembersCount()),
   265  		Color:        community.Identity().GetColor(),
   266  		TagIndices:   community.TagsIndices(),
   267  	}
   268  
   269  	channelProto := &protobuf.Channel{
   270  		DisplayName: channel.Identity.DisplayName,
   271  		Description: channel.Identity.Description,
   272  		Emoji:       channel.Identity.Emoji,
   273  		Color:       channel.GetIdentity().Color,
   274  		Community:   communityProto,
   275  		Uuid:        channelID,
   276  	}
   277  
   278  	channelData, err := proto.Marshal(channelProto)
   279  	if err != nil {
   280  		return "", "", err
   281  	}
   282  
   283  	urlDataProto := &protobuf.URLData{
   284  		Content: channelData,
   285  		Shard:   community.Shard().Protobuffer(),
   286  	}
   287  
   288  	urlData, err := proto.Marshal(urlDataProto)
   289  	if err != nil {
   290  		return "", "", err
   291  	}
   292  
   293  	shortKey, err := serializePublicKey(community.ID())
   294  	if err != nil {
   295  		return "", "", err
   296  	}
   297  	encodedData, err := encodeDataURL(urlData)
   298  	if err != nil {
   299  		return "", "", err
   300  	}
   301  
   302  	return encodedData, shortKey, nil
   303  }
   304  
   305  func (m *Messenger) ShareCommunityChannelURLWithData(request *requests.CommunityChannelShareURL) (string, error) {
   306  	if err := request.Validate(); err != nil {
   307  		return "", err
   308  	}
   309  
   310  	valid, err := regexp.MatchString(channelUUIDRegExp, request.ChannelID)
   311  	if err != nil {
   312  		return "", err
   313  	}
   314  
   315  	if !valid {
   316  		return "nil", fmt.Errorf("channelID should be UUID, got %s", request.ChannelID)
   317  	}
   318  
   319  	community, err := m.GetCommunityByID(request.CommunityID)
   320  	if err != nil {
   321  		return "", err
   322  	}
   323  
   324  	channel := community.Chats()[request.ChannelID]
   325  	if channel == nil {
   326  		return "", fmt.Errorf("channel with channelID %s not found", request.ChannelID)
   327  	}
   328  
   329  	data, shortKey, err := m.prepareEncodedCommunityChannelData(community, channel, request.ChannelID)
   330  	if err != nil {
   331  		return "", err
   332  	}
   333  
   334  	return fmt.Sprintf("%s/cc/%s#%s", baseShareURL, data, shortKey), nil
   335  }
   336  
   337  func parseCommunityChannelURLWithData(data string, chatKey string) (*URLDataResponse, error) {
   338  	communityID, err := deserializePublicKey(chatKey)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  
   343  	urlData, err := decodeDataURL(data)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	var urlDataProto protobuf.URLData
   349  	err = proto.Unmarshal(urlData, &urlDataProto)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	var channelProto protobuf.Channel
   355  	err = proto.Unmarshal(urlDataProto.Content, &channelProto)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	var tagIndices []uint32
   361  	if channelProto.Community.TagIndices != nil {
   362  		tagIndices = channelProto.Community.TagIndices
   363  	} else {
   364  		tagIndices = []uint32{}
   365  	}
   366  
   367  	return &URLDataResponse{
   368  		Community: &CommunityURLData{
   369  			DisplayName:  channelProto.Community.DisplayName,
   370  			Description:  channelProto.Community.Description,
   371  			MembersCount: channelProto.Community.MembersCount,
   372  			Color:        channelProto.Community.Color,
   373  			TagIndices:   tagIndices,
   374  			CommunityID:  types.EncodeHex(communityID),
   375  		},
   376  		Channel: &CommunityChannelURLData{
   377  			Emoji:       channelProto.Emoji,
   378  			DisplayName: channelProto.DisplayName,
   379  			Description: channelProto.Description,
   380  			Color:       channelProto.Color,
   381  			ChannelUUID: channelProto.Uuid,
   382  		},
   383  		Shard: shard.FromProtobuff(urlDataProto.Shard),
   384  	}, nil
   385  }
   386  
   387  func (m *Messenger) ShareUserURLWithChatKey(contactID string) (string, error) {
   388  	publicKey, err := common.HexToPubkey(contactID)
   389  	if err != nil {
   390  		return "", err
   391  	}
   392  
   393  	shortKey, err := serializePublicKey(crypto.CompressPubkey(publicKey))
   394  	if err != nil {
   395  		return "", err
   396  	}
   397  
   398  	return fmt.Sprintf("%s/u#%s", baseShareURL, shortKey), nil
   399  }
   400  
   401  func parseUserURLWithChatKey(urlData string) (*URLDataResponse, error) {
   402  	pubKeyBytes, err := deserializePublicKey(urlData)
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	pubKey, err := crypto.DecompressPubkey(pubKeyBytes)
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  
   412  	serializedPublicKey, err := multiformat.SerializeLegacyKey(common.PubkeyToHex(pubKey))
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	return &URLDataResponse{
   418  		Contact: &ContactURLData{
   419  			PublicKey: serializedPublicKey,
   420  		},
   421  	}, nil
   422  }
   423  
   424  func (m *Messenger) ShareUserURLWithENS(contactID string) (string, error) {
   425  	contact := m.GetContactByID(contactID)
   426  	if contact == nil {
   427  		return "", ErrContactNotFound
   428  	}
   429  	return fmt.Sprintf("%s/u#%s", baseShareURL, contact.EnsName), nil
   430  }
   431  
   432  func parseUserURLWithENS(ensName string) (*URLDataResponse, error) {
   433  	// TODO: fetch contact by ens name
   434  	return nil, fmt.Errorf("not implemented yet")
   435  }
   436  
   437  func (m *Messenger) prepareEncodedUserData(contact *Contact) (string, string, error) {
   438  	pk, err := contact.PublicKey()
   439  	if err != nil {
   440  		return "", "", err
   441  	}
   442  
   443  	shortKey, err := serializePublicKey(crypto.CompressPubkey(pk))
   444  	if err != nil {
   445  		return "", "", err
   446  	}
   447  
   448  	userProto := &protobuf.User{
   449  		DisplayName: contact.DisplayName,
   450  		Description: contact.Bio,
   451  	}
   452  
   453  	userData, err := proto.Marshal(userProto)
   454  	if err != nil {
   455  		return "", "", err
   456  	}
   457  
   458  	urlDataProto := &protobuf.URLData{
   459  		Content: userData,
   460  	}
   461  
   462  	urlData, err := proto.Marshal(urlDataProto)
   463  	if err != nil {
   464  		return "", "", err
   465  	}
   466  
   467  	encodedData, err := encodeDataURL(urlData)
   468  	if err != nil {
   469  		return "", "", err
   470  	}
   471  
   472  	return encodedData, shortKey, nil
   473  }
   474  
   475  func (m *Messenger) ShareUserURLWithData(contactID string) (string, error) {
   476  	contact := m.GetContactByID(contactID)
   477  	if contact == nil {
   478  		return "", ErrContactNotFound
   479  	}
   480  
   481  	data, shortKey, err := m.prepareEncodedUserData(contact)
   482  	if err != nil {
   483  		return "", err
   484  	}
   485  
   486  	return fmt.Sprintf("%s/u/%s#%s", baseShareURL, data, shortKey), nil
   487  }
   488  
   489  func parseUserURLWithData(data string, chatKey string) (*URLDataResponse, error) {
   490  	urlData, err := decodeDataURL(data)
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  
   495  	var urlDataProto protobuf.URLData
   496  	err = proto.Unmarshal(urlData, &urlDataProto)
   497  	if err != nil {
   498  		return nil, err
   499  	}
   500  
   501  	var userProto protobuf.User
   502  	err = proto.Unmarshal(urlDataProto.Content, &userProto)
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  
   507  	return &URLDataResponse{
   508  		Contact: &ContactURLData{
   509  			DisplayName: userProto.DisplayName,
   510  			Description: userProto.Description,
   511  			PublicKey:   chatKey,
   512  		},
   513  	}, nil
   514  }
   515  
   516  func IsStatusSharedURL(url string) bool {
   517  	return strings.HasPrefix(url, sharedURLUserPrefix) ||
   518  		strings.HasPrefix(url, sharedURLUserPrefixWithData) ||
   519  		strings.HasPrefix(url, sharedURLCommunityPrefix) ||
   520  		strings.HasPrefix(url, sharedURLCommunityPrefixWithData) ||
   521  		strings.HasPrefix(url, sharedURLChannelPrefixWithData)
   522  }
   523  
   524  func splitSharedURLData(data string) (string, string, error) {
   525  	const count = 2
   526  	contents := strings.SplitN(data, "#", count)
   527  	if len(contents) != count {
   528  		return "", "", fmt.Errorf("url should contain at least one `#` separator")
   529  	}
   530  	return contents[0], contents[1], nil
   531  }
   532  
   533  func ParseSharedURL(url string) (*URLDataResponse, error) {
   534  
   535  	if strings.HasPrefix(url, sharedURLUserPrefix) {
   536  		chatKey := strings.TrimPrefix(url, sharedURLUserPrefix)
   537  		if strings.HasPrefix(chatKey, "zQ3sh") {
   538  			return parseUserURLWithChatKey(chatKey)
   539  		}
   540  		return parseUserURLWithENS(chatKey)
   541  	}
   542  
   543  	if strings.HasPrefix(url, sharedURLUserPrefixWithData) {
   544  		trimmedURL := strings.TrimPrefix(url, sharedURLUserPrefixWithData)
   545  		encodedData, chatKey, err := splitSharedURLData(trimmedURL)
   546  		if err != nil {
   547  			return nil, err
   548  		}
   549  		return parseUserURLWithData(encodedData, chatKey)
   550  	}
   551  
   552  	if strings.HasPrefix(url, sharedURLCommunityPrefix) {
   553  		chatKey := strings.TrimPrefix(url, sharedURLCommunityPrefix)
   554  		return parseCommunityURLWithChatKey(chatKey)
   555  	}
   556  
   557  	if strings.HasPrefix(url, sharedURLCommunityPrefixWithData) {
   558  		trimmedURL := strings.TrimPrefix(url, sharedURLCommunityPrefixWithData)
   559  		encodedData, chatKey, err := splitSharedURLData(trimmedURL)
   560  		if err != nil {
   561  			return nil, err
   562  		}
   563  		return parseCommunityURLWithData(encodedData, chatKey)
   564  	}
   565  
   566  	if strings.HasPrefix(url, sharedURLChannelPrefixWithData) {
   567  		trimmedURL := strings.TrimPrefix(url, sharedURLChannelPrefixWithData)
   568  		encodedData, chatKey, err := splitSharedURLData(trimmedURL)
   569  		if err != nil {
   570  			return nil, err
   571  		}
   572  
   573  		if channelRegExp.MatchString(encodedData) {
   574  			return parseCommunityChannelURLWithChatKey(encodedData, chatKey)
   575  		}
   576  		return parseCommunityChannelURLWithData(encodedData, chatKey)
   577  	}
   578  
   579  	return nil, fmt.Errorf("not a status shared url")
   580  }
   581  
   582  func encodeDataURL(data []byte) (string, error) {
   583  	bb := bytes.NewBuffer([]byte{})
   584  	writer := brotli.NewWriter(bb)
   585  	_, err := writer.Write(data)
   586  	if err != nil {
   587  		return "", err
   588  	}
   589  
   590  	err = writer.Close()
   591  	if err != nil {
   592  		return "", err
   593  	}
   594  
   595  	return base64.URLEncoding.EncodeToString(bb.Bytes()), nil
   596  }
   597  
   598  func decodeDataURL(data string) ([]byte, error) {
   599  	decoded, err := base64.URLEncoding.DecodeString(data)
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  
   604  	output := make([]byte, 4096)
   605  	bb := bytes.NewBuffer(decoded)
   606  	reader := brotli.NewReader(bb)
   607  	n, err := reader.Read(output)
   608  	if err != nil {
   609  		return nil, err
   610  	}
   611  
   612  	return output[:n], nil
   613  }