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 }