feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions

View File

@@ -0,0 +1,23 @@
package common
import (
"crypto/ecdsa"
"github.com/golang/protobuf/proto"
"github.com/status-im/status-go/protocol/protobuf"
)
// ChatEntity is anything that is sendable in a chat.
// Currently it encompass a Message and EmojiReaction.
type ChatEntity interface {
proto.Message
GetChatId() string
GetMessageType() protobuf.MessageType
GetSigPubKey() *ecdsa.PublicKey
GetProtobuf() proto.Message
WrapGroupMessage() bool
SetMessageType(messageType protobuf.MessageType)
}

View File

@@ -0,0 +1,133 @@
package common
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/rand"
"errors"
"io"
"math/big"
"golang.org/x/crypto/sha3"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
)
const (
nonceLength = 12
defaultECHDSharedKeyLength = 16
defaultECHDMACLength = 16
)
var (
ErrInvalidCiphertextLength = errors.New("invalid cyphertext length")
letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
numberRunes = []rune("0123456789")
alphanumericRunes = append(numberRunes, letterRunes...)
)
func HashPublicKey(pk *ecdsa.PublicKey) []byte {
return Shake256(crypto.CompressPubkey(pk))
}
func Decrypt(cyphertext []byte, key []byte) ([]byte, error) {
if len(cyphertext) < nonceLength {
return nil, ErrInvalidCiphertextLength
}
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonce := cyphertext[:nonceLength]
return gcm.Open(nil, nonce, cyphertext[nonceLength:], nil)
}
func Encrypt(plaintext []byte, key []byte, reader io.Reader) ([]byte, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func Shake256(buf []byte) []byte {
h := make([]byte, 64)
sha3.ShakeSum256(h, buf)
return h
}
// IsPubKeyEqual checks that two public keys are equal
func IsPubKeyEqual(a, b *ecdsa.PublicKey) bool {
// the curve is always the same, just compare the points
return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0
}
func PubkeyToHex(key *ecdsa.PublicKey) string {
return types.EncodeHex(crypto.FromECDSAPub(key))
}
func PubkeyToHexBytes(key *ecdsa.PublicKey) types.HexBytes {
return crypto.FromECDSAPub(key)
}
func HexToPubkey(pk string) (*ecdsa.PublicKey, error) {
bytes, err := types.DecodeHex(pk)
if err != nil {
return nil, err
}
return crypto.UnmarshalPubkey(bytes)
}
func MakeECDHSharedKey(yourPrivateKey *ecdsa.PrivateKey, theirPubKey *ecdsa.PublicKey) ([]byte, error) {
return ecies.ImportECDSA(yourPrivateKey).GenerateShared(
ecies.ImportECDSAPublic(theirPubKey),
defaultECHDSharedKeyLength,
defaultECHDMACLength,
)
}
func randomString(choice []rune, n int) (string, error) {
max := big.NewInt(int64(len(choice)))
rr := rand.Reader
b := make([]rune, n)
for i := range b {
pos, err := rand.Int(rr, max)
if err != nil {
return "", err
}
b[i] = choice[pos.Int64()]
}
return string(b), nil
}
func RandomAlphabeticalString(n int) (string, error) {
return randomString(letterRunes, n)
}
func RandomAlphanumericString(n int) (string, error) {
return randomString(alphanumericRunes, n)
}

View File

@@ -0,0 +1,5 @@
package common
import "errors"
var ErrRecordNotFound = errors.New("record not found")

View File

@@ -0,0 +1,33 @@
package common
type FeatureFlags struct {
// Datasync indicates whether direct messages should be sent exclusively
// using datasync, breaking change for non-v1 clients. Public messages
// are not impacted
Datasync bool
// PushNotification indicates whether we should be enabling the push notification feature
PushNotifications bool
// MailserverCycle indicates whether we should enable or not the mailserver cycle
MailserverCycle bool
// DisableCheckingForBackup disables backup loop
DisableCheckingForBackup bool
// DisableAutoMessageLoop disables auto message loop
DisableAutoMessageLoop bool
// ResendRawMessagesDisabled indicates whether we should be disabling sending raw messages
ResendRawMessagesDisabled bool
// StoreNodesDisabled indicates whether we should fetch messages from store nodes
StoreNodesDisabled bool
// Peersyncing indicates whether we should advertise and sync messages with other peers
Peersyncing bool
// AutoRequestHistoricMessages indicates whether we should automatically request
// historic messages on getting online, connecting to store node, etc.
AutoRequestHistoricMessages bool
}

View File

@@ -0,0 +1,28 @@
package common
type MediaServerImageIDPrefix string
type MediaServerImageIDPostfix string
type MediaServerImageID string
func CreateImageID(prefix MediaServerImageIDPrefix, postfix MediaServerImageIDPostfix) MediaServerImageID {
return MediaServerImageID(string(prefix) + string(postfix))
}
const (
MediaServerIconPostfix MediaServerImageIDPostfix = "icon"
MediaServerBannerPostfix MediaServerImageIDPostfix = "banner"
)
const (
MediaServerContactPrefix MediaServerImageIDPrefix = "contact-"
MediaServerCommunityPrefix MediaServerImageIDPrefix = "community-"
MediaServerChannelCommunityPrefix MediaServerImageIDPrefix = "community-channel-"
)
const (
MediaServerContactIcon = MediaServerImageID(string(MediaServerContactPrefix) + string(MediaServerIconPostfix))
MediaServerCommunityIcon = MediaServerImageID(string(MediaServerCommunityPrefix) + string(MediaServerIconPostfix))
MediaServerCommunityBanner = MediaServerImageID(string(MediaServerCommunityPrefix) + string(MediaServerBannerPostfix))
MediaServerChannelCommunityIcon = MediaServerImageID(string(MediaServerChannelCommunityPrefix) + string(MediaServerIconPostfix))
MediaServerChannelCommunityBanner = MediaServerImageID(string(MediaServerChannelCommunityPrefix) + string(MediaServerBannerPostfix))
)

View File

@@ -0,0 +1,771 @@
package common
import (
"crypto/ecdsa"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"unicode"
"unicode/utf8"
"github.com/golang/protobuf/proto"
"github.com/status-im/markdown"
"github.com/status-im/markdown/ast"
accountJson "github.com/status-im/status-go/account/json"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol/audio"
"github.com/status-im/status-go/protocol/protobuf"
)
// QuotedMessage contains the original text of the message replied to
type QuotedMessage struct {
ID string `json:"id"`
ContentType int64 `json:"contentType"`
// From is a public key of the author of the message.
From string `json:"from"`
Text string `json:"text"`
ParsedText json.RawMessage `json:"parsedText,omitempty"`
AlbumImages json.RawMessage `json:"albumImages,omitempty"`
AlbumImagesCount int64 `json:"albumImagesCount"`
// ImageLocalURL is the local url of the image
ImageLocalURL string `json:"image,omitempty"`
// AudioLocalURL is the local url of the audio
AudioLocalURL string `json:"audio,omitempty"`
HasSticker bool `json:"sticker,omitempty"`
// CommunityID is the id of the community advertised
CommunityID string `json:"communityId,omitempty"`
Deleted bool `json:"deleted,omitempty"`
DeletedForMe bool `json:"deletedForMe,omitempty"`
DiscordMessage *protobuf.DiscordMessage `json:"discordMessage,omitempty"`
}
type CommandState int
const (
CommandStateRequestAddressForTransaction CommandState = iota + 1
CommandStateRequestAddressForTransactionDeclined
CommandStateRequestAddressForTransactionAccepted
CommandStateRequestTransaction
CommandStateRequestTransactionDeclined
CommandStateTransactionPending
CommandStateTransactionSent
)
type ContactRequestState int
const (
ContactRequestStatePending ContactRequestState = iota + 1
ContactRequestStateAccepted
ContactRequestStateDismissed
)
type ContactVerificationState int
const (
ContactVerificationStatePending ContactVerificationState = iota + 1
ContactVerificationStateAccepted
ContactVerificationStateDeclined
ContactVerificationStateTrusted
ContactVerificationStateUntrustworthy
ContactVerificationStateCanceled
)
const EveryoneMentionTag = "0x00001"
type CommandParameters struct {
// ID is the ID of the initial message
ID string `json:"id"`
// From is the address we are sending the command from
From string `json:"from"`
// Address is the address sent with the command
Address string `json:"address"`
// Contract is the contract address for ERC20 tokens
Contract string `json:"contract"`
// Value is the value as a string sent
Value string `json:"value"`
// TransactionHash is the hash of the transaction
TransactionHash string `json:"transactionHash"`
// CommandState is the state of the command
CommandState CommandState `json:"commandState"`
// The Signature of the pk-bytes+transaction-hash from the wallet
// address originating
Signature []byte `json:"signature"`
}
// GapParameters is the From and To indicating the missing period in chat history
type GapParameters struct {
From uint32 `json:"from,omitempty"`
To uint32 `json:"to,omitempty"`
}
func (c *CommandParameters) IsTokenTransfer() bool {
return len(c.Contract) != 0
}
const (
OutgoingStatusSending = "sending"
OutgoingStatusSent = "sent"
OutgoingStatusDelivered = "delivered"
)
type Messages []*Message
func (m Messages) GetClock(i int) uint64 {
return m[i].Clock
}
// Message represents a message record in the database,
// more specifically in user_messages table.
type Message struct {
*protobuf.ChatMessage
// ID calculated as keccak256(compressedAuthorPubKey, data) where data is unencrypted payload.
ID string `json:"id"`
// WhisperTimestamp is a timestamp of a Whisper envelope.
WhisperTimestamp uint64 `json:"whisperTimestamp"`
// From is a public key of the author of the message.
From string `json:"from"`
// Random 3 words name
Alias string `json:"alias"`
// Identicon of the author
Identicon string `json:"identicon"`
// The chat id to be stored locally
LocalChatID string `json:"localChatId"`
// Seen set to true when user have read this message already
Seen bool `json:"seen"`
OutgoingStatus string `json:"outgoingStatus,omitempty"`
QuotedMessage *QuotedMessage `json:"quotedMessage"`
// CommandParameters is the parameters sent with the message
CommandParameters *CommandParameters `json:"commandParameters"`
// GapParameters is the value from/to related to the gap
GapParameters *GapParameters `json:"gapParameters,omitempty"`
// Computed fields
// RTL is whether this is a right-to-left message (arabic/hebrew script etc)
RTL bool `json:"rtl"`
// ParsedText is the parsed markdown for displaying
ParsedText []byte `json:"parsedText,omitempty"`
// ParsedTextAst is the ast of the parsed text
ParsedTextAst *ast.Node `json:"-"`
// LineCount is the count of newlines in the message
LineCount int `json:"lineCount"`
// Base64Image is the converted base64 image
Base64Image string `json:"image,omitempty"`
// ImagePath is the path of the image to be sent
ImagePath string `json:"imagePath,omitempty"`
// Base64Audio is the converted base64 audio
Base64Audio string `json:"audio,omitempty"`
// AudioPath is the path of the audio to be sent
AudioPath string `json:"audioPath,omitempty"`
// ImageLocalURL is the local url of the image
ImageLocalURL string `json:"imageLocalUrl,omitempty"`
// AudioLocalURL is the local url of the audio
AudioLocalURL string `json:"audioLocalUrl,omitempty"`
// StickerLocalURL is the local url of the sticker
StickerLocalURL string `json:"stickerLocalUrl,omitempty"`
// CommunityID is the id of the community to advertise
CommunityID string `json:"communityId,omitempty"`
// Replace indicates that this is a replacement of a message
// that has been updated
Replace string `json:"replace,omitempty"`
New bool `json:"new,omitempty"`
SigPubKey *ecdsa.PublicKey `json:"-"`
// Mentions is an array of mentions for a given message
Mentions []string
// Mentioned is whether the user is mentioned in the message
Mentioned bool `json:"mentioned"`
// Replied is whether the user is replied to in the message
Replied bool `json:"replied"`
// Links is an array of links within given message
Links []string
LinkPreviews []LinkPreview `json:"linkPreviews"`
StatusLinkPreviews []StatusLinkPreview `json:"statusLinkPreviews"`
// EditedAt indicates the clock value it was edited
EditedAt uint64 `json:"editedAt"`
// Deleted indicates if a message was deleted
Deleted bool `json:"deleted"`
DeletedBy string `json:"deletedBy,omitempty"`
DeletedForMe bool `json:"deletedForMe"`
// ContactRequestState is the state of the contact request message
ContactRequestState ContactRequestState `json:"contactRequestState,omitempty"`
// ContactVerificationState is the state of the identity verification process
ContactVerificationState ContactVerificationState `json:"contactVerificationState,omitempty"`
DiscordMessage *protobuf.DiscordMessage `json:"discordMessage,omitempty"`
}
func (m *Message) MarshalJSON() ([]byte, error) {
type StickerAlias struct {
Hash string `json:"hash"`
Pack int32 `json:"pack"`
URL string `json:"url"`
}
if m.ChatMessage == nil {
m.ChatMessage = &protobuf.ChatMessage{}
}
type MessageStructType struct {
ID string `json:"id"`
WhisperTimestamp uint64 `json:"whisperTimestamp"`
From string `json:"from"`
Alias string `json:"alias"`
Identicon string `json:"identicon"`
Seen bool `json:"seen"`
OutgoingStatus string `json:"outgoingStatus,omitempty"`
QuotedMessage *QuotedMessage `json:"quotedMessage"`
RTL bool `json:"rtl"`
ParsedText json.RawMessage `json:"parsedText,omitempty"`
LineCount int `json:"lineCount"`
Text string `json:"text"`
ChatID string `json:"chatId"`
LocalChatID string `json:"localChatId"`
Clock uint64 `json:"clock"`
Replace string `json:"replace"`
ResponseTo string `json:"responseTo"`
New bool `json:"new,omitempty"`
EnsName string `json:"ensName"`
DisplayName string `json:"displayName"`
Image string `json:"image,omitempty"`
AlbumID string `json:"albumId,omitempty"`
ImageWidth uint32 `json:"imageWidth,omitempty"`
ImageHeight uint32 `json:"imageHeight,omitempty"`
AlbumImagesCount uint32 `json:"albumImagesCount,omitempty"`
Audio string `json:"audio,omitempty"`
AudioDurationMs uint64 `json:"audioDurationMs,omitempty"`
CommunityID string `json:"communityId,omitempty"`
Sticker *StickerAlias `json:"sticker,omitempty"`
CommandParameters *CommandParameters `json:"commandParameters,omitempty"`
GapParameters *GapParameters `json:"gapParameters,omitempty"`
Timestamp uint64 `json:"timestamp"`
ContentType protobuf.ChatMessage_ContentType `json:"contentType"`
MessageType protobuf.MessageType `json:"messageType"`
Mentions []string `json:"mentions,omitempty"`
Mentioned bool `json:"mentioned,omitempty"`
Replied bool `json:"replied,omitempty"`
Links []string `json:"links,omitempty"`
LinkPreviews []LinkPreview `json:"linkPreviews,omitempty"`
StatusLinkPreviews []StatusLinkPreview `json:"statusLinkPreviews,omitempty"`
EditedAt uint64 `json:"editedAt,omitempty"`
Deleted bool `json:"deleted,omitempty"`
DeletedBy string `json:"deletedBy,omitempty"`
DeletedForMe bool `json:"deletedForMe,omitempty"`
ContactRequestState ContactRequestState `json:"contactRequestState,omitempty"`
ContactVerificationState ContactVerificationState `json:"contactVerificationState,omitempty"`
DiscordMessage *protobuf.DiscordMessage `json:"discordMessage,omitempty"`
BridgeMessage *protobuf.BridgeMessage `json:"bridgeMessage,omitempty"`
}
item := MessageStructType{
ID: m.ID,
WhisperTimestamp: m.WhisperTimestamp,
From: m.From,
Alias: m.Alias,
Identicon: m.Identicon,
Seen: m.Seen,
OutgoingStatus: m.OutgoingStatus,
QuotedMessage: m.QuotedMessage,
RTL: m.RTL,
ParsedText: m.ParsedText,
LineCount: m.LineCount,
Text: m.Text,
Replace: m.Replace,
ChatID: m.ChatId,
LocalChatID: m.LocalChatID,
Clock: m.Clock,
ResponseTo: m.ResponseTo,
New: m.New,
EnsName: m.EnsName,
DisplayName: m.DisplayName,
Image: m.ImageLocalURL,
Audio: m.AudioLocalURL,
CommunityID: m.CommunityID,
Timestamp: m.Timestamp,
ContentType: m.ContentType,
Mentions: m.Mentions,
Mentioned: m.Mentioned,
Replied: m.Replied,
Links: m.Links,
LinkPreviews: m.LinkPreviews,
StatusLinkPreviews: m.StatusLinkPreviews,
MessageType: m.MessageType,
CommandParameters: m.CommandParameters,
GapParameters: m.GapParameters,
EditedAt: m.EditedAt,
Deleted: m.Deleted,
DeletedBy: m.DeletedBy,
DeletedForMe: m.DeletedForMe,
ContactRequestState: m.ContactRequestState,
ContactVerificationState: m.ContactVerificationState,
}
if sticker := m.GetSticker(); sticker != nil {
item.Sticker = &StickerAlias{
Pack: sticker.Pack,
Hash: sticker.Hash,
URL: m.StickerLocalURL,
}
}
if audio := m.GetAudio(); audio != nil {
item.AudioDurationMs = audio.DurationMs
}
if image := m.GetImage(); image != nil {
item.AlbumID = image.AlbumId
item.ImageWidth = image.Width
item.ImageHeight = image.Height
item.AlbumImagesCount = image.AlbumImagesCount
}
if discordMessage := m.GetDiscordMessage(); discordMessage != nil {
item.DiscordMessage = discordMessage
}
if bridgeMessage := m.GetBridgeMessage(); bridgeMessage != nil {
item.BridgeMessage = bridgeMessage
}
if item.From != "" {
ext, err := accountJson.ExtendStructWithPubKeyData(item.From, item)
if err != nil {
return nil, err
}
return json.Marshal(ext)
}
return json.Marshal(item)
}
func (m *Message) UnmarshalJSON(data []byte) error {
type Alias Message
aux := struct {
*Alias
ResponseTo string `json:"responseTo"`
EnsName string `json:"ensName"`
DisplayName string `json:"displayName"`
ChatID string `json:"chatId"`
Sticker *protobuf.StickerMessage `json:"sticker"`
AudioDurationMs uint64 `json:"audioDurationMs"`
ParsedText json.RawMessage `json:"parsedText"`
ContentType protobuf.ChatMessage_ContentType `json:"contentType"`
AlbumID string `json:"albumId"`
ImageWidth uint32 `json:"imageWidth"`
ImageHeight uint32 `json:"imageHeight"`
AlbumImagesCount uint32 `json:"albumImagesCount"`
From string `json:"from"`
Deleted bool `json:"deleted,omitempty"`
DeletedForMe bool `json:"deletedForMe,omitempty"`
}{
Alias: (*Alias)(m),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ContentType == protobuf.ChatMessage_STICKER {
m.Payload = &protobuf.ChatMessage_Sticker{Sticker: aux.Sticker}
}
if aux.ContentType == protobuf.ChatMessage_AUDIO {
m.Payload = &protobuf.ChatMessage_Audio{
Audio: &protobuf.AudioMessage{DurationMs: aux.AudioDurationMs},
}
}
if aux.ContentType == protobuf.ChatMessage_IMAGE {
m.Payload = &protobuf.ChatMessage_Image{
Image: &protobuf.ImageMessage{
AlbumId: aux.AlbumID,
Width: aux.ImageWidth,
Height: aux.ImageHeight,
AlbumImagesCount: aux.AlbumImagesCount},
}
}
m.ResponseTo = aux.ResponseTo
m.EnsName = aux.EnsName
m.DisplayName = aux.DisplayName
m.ChatId = aux.ChatID
m.ContentType = aux.ContentType
m.ParsedText = aux.ParsedText
m.From = aux.From
m.Deleted = aux.Deleted
m.DeletedForMe = aux.DeletedForMe
return nil
}
// Check if the first character is Hebrew or Arabic or the RTL character
func isRTL(s string) bool {
first, _ := utf8.DecodeRuneInString(s)
return unicode.Is(unicode.Hebrew, first) ||
unicode.Is(unicode.Arabic, first) ||
// RTL character
first == '\u200f'
}
// parseImage check the message contains an image, and if so
// it creates the a base64 encoded version of it.
func (m *Message) parseImage() error {
if m.ContentType != protobuf.ChatMessage_IMAGE {
return nil
}
image := m.GetImage()
if image == nil {
return errors.New("image empty")
}
payload := image.Payload
e64 := base64.StdEncoding
maxEncLen := e64.EncodedLen(len(payload))
encBuf := make([]byte, maxEncLen)
e64.Encode(encBuf, payload)
mime, err := images.GetMimeType(image.Payload)
if err != nil {
return err
}
m.Base64Image = fmt.Sprintf("data:image/%s;base64,%s", mime, encBuf)
return nil
}
// parseAudio check the message contains an audio, and if so
// it creates a base64 encoded version of it.
func (m *Message) parseAudio() error {
if m.ContentType != protobuf.ChatMessage_AUDIO {
return nil
}
audio := m.GetAudio()
if audio == nil {
return errors.New("audio empty")
}
payload := audio.Payload
e64 := base64.StdEncoding
maxEncLen := e64.EncodedLen(len(payload))
encBuf := make([]byte, maxEncLen)
e64.Encode(encBuf, payload)
mime, err := getAudioMessageMIME(audio)
if err != nil {
return err
}
m.Base64Audio = fmt.Sprintf("data:audio/%s;base64,%s", mime, encBuf)
return nil
}
// implement interface of https://github.com/status-im/markdown/blob/b9fe921681227b1dace4b56364e15edb3b698308/ast/node.go#L701
type SimplifiedTextVisitor struct {
text string
canonicalNames map[string]string
}
func (v *SimplifiedTextVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus {
// only on entering we fetch, otherwise we go on
if !entering {
return ast.GoToNext
}
switch n := node.(type) {
case *ast.Mention:
literal := string(n.Literal)
canonicalName, ok := v.canonicalNames[literal]
if ok {
v.text += canonicalName
} else {
v.text += literal
}
case *ast.Link:
destination := string(n.Destination)
v.text += destination
default:
var literal string
leaf := node.AsLeaf()
container := node.AsContainer()
if leaf != nil {
literal = string(leaf.Literal)
} else if container != nil {
literal = string(container.Literal)
}
v.text += literal
}
return ast.GoToNext
}
// implement interface of https://github.com/status-im/markdown/blob/b9fe921681227b1dace4b56364e15edb3b698308/ast/node.go#L701
type MentionsAndLinksVisitor struct {
identity string
mentioned bool
mentions []string
links []string
}
type LinksVisitor struct {
Links []string
}
func (v *MentionsAndLinksVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus {
// only on entering we fetch, otherwise we go on
if !entering {
return ast.GoToNext
}
switch n := node.(type) {
case *ast.Mention:
mention := string(n.Literal)
if mention == v.identity || mention == EveryoneMentionTag {
v.mentioned = true
}
v.mentions = append(v.mentions, mention)
case *ast.Link:
v.links = append(v.links, string(n.Destination))
}
return ast.GoToNext
}
func (v *LinksVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.GoToNext
}
switch n := node.(type) {
case *ast.Link:
v.Links = append(v.Links, string(n.Destination))
}
return ast.GoToNext
}
func runMentionsAndLinksVisitor(parsedText ast.Node, identity string) *MentionsAndLinksVisitor {
visitor := &MentionsAndLinksVisitor{identity: identity}
ast.Walk(parsedText, visitor)
return visitor
}
func RunLinksVisitor(parsedText ast.Node) *LinksVisitor {
visitor := &LinksVisitor{}
ast.Walk(parsedText, visitor)
return visitor
}
// PrepareContent return the parsed content of the message, the line-count and whether
// is a right-to-left message
func (m *Message) PrepareContent(identity string) error {
var parsedText ast.Node
switch m.ContentType {
case protobuf.ChatMessage_DISCORD_MESSAGE:
parsedText = markdown.Parse([]byte(m.GetDiscordMessage().Content), nil)
default:
parsedText = markdown.Parse([]byte(m.Text), nil)
}
visitor := runMentionsAndLinksVisitor(parsedText, identity)
m.Mentions = visitor.mentions
m.Links = visitor.links
// Leave it set if already set, as sometimes we might run this without
// an identity
if !m.Mentioned || identity != "" {
m.Mentioned = visitor.mentioned
}
jsonParsedText, err := json.Marshal(parsedText)
if err != nil {
return err
}
m.ParsedTextAst = &parsedText
m.ParsedText = jsonParsedText
m.LineCount = strings.Count(m.Text, "\n")
m.RTL = isRTL(m.Text)
if err := m.parseImage(); err != nil {
return err
}
return m.parseAudio()
}
// GetSimplifiedText returns a the text stripped of all the markdown and with mentions
// replaced by canonical names
func (m *Message) GetSimplifiedText(identity string, canonicalNames map[string]string) (string, error) {
if m.ContentType == protobuf.ChatMessage_AUDIO {
return "Audio", nil
}
if m.ContentType == protobuf.ChatMessage_STICKER {
return "Sticker", nil
}
if m.ContentType == protobuf.ChatMessage_IMAGE {
return "Image", nil
}
if m.ContentType == protobuf.ChatMessage_COMMUNITY {
return "Community", nil
}
if m.ContentType == protobuf.ChatMessage_SYSTEM_MESSAGE_CONTENT_PRIVATE_GROUP {
return "Group", nil
}
if m.ParsedTextAst == nil {
err := m.PrepareContent(identity)
if err != nil {
return "", err
}
}
visitor := &SimplifiedTextVisitor{canonicalNames: canonicalNames}
ast.Walk(*m.ParsedTextAst, visitor)
return visitor.text, nil
}
func getAudioMessageMIME(i *protobuf.AudioMessage) (string, error) {
switch i.Type {
case protobuf.AudioMessage_AAC:
return "aac", nil
case protobuf.AudioMessage_AMR:
return "amr", nil
}
return "", errors.New("audio format not supported")
}
// GetSigPubKey returns an ecdsa encoded public key
// this function is required to implement the ChatEntity interface
func (m *Message) GetSigPubKey() *ecdsa.PublicKey {
return m.SigPubKey
}
// GetProtoBuf returns the struct's embedded protobuf struct
// this function is required to implement the ChatEntity interface
func (m *Message) GetProtobuf() proto.Message {
return m.ChatMessage
}
// SetMessageType a setter for the MessageType field
// this function is required to implement the ChatEntity interface
func (m *Message) SetMessageType(messageType protobuf.MessageType) {
m.MessageType = messageType
}
// WrapGroupMessage indicates whether we should wrap this in membership information
func (m *Message) WrapGroupMessage() bool {
return true
}
// GetPublicKey attempts to return or recreate the *ecdsa.PublicKey of the Message sender.
// If the m.SigPubKey is set this will be returned
// If the m.From is present the string is decoded and unmarshalled into a *ecdsa.PublicKey, the m.SigPubKey is set and returned
// Else an error is thrown
// This function differs from GetSigPubKey() as this function may return an error
func (m *Message) GetSenderPubKey() (*ecdsa.PublicKey, error) {
// TODO requires tests
if m.SigPubKey != nil {
return m.SigPubKey, nil
}
if len(m.From) > 0 {
fromB, err := hex.DecodeString(m.From[2:])
if err != nil {
return nil, err
}
senderPubKey, err := crypto.UnmarshalPubkey(fromB)
if err != nil {
return nil, err
}
m.SigPubKey = senderPubKey
return senderPubKey, nil
}
return nil, errors.New("no Message.SigPubKey or Message.From set unable to get public key")
}
func (m *Message) LoadAudio() error {
file, err := os.Open(m.AudioPath)
if err != nil {
return err
}
defer file.Close()
payload, err := ioutil.ReadAll(file)
if err != nil {
return err
}
audioMessage := m.GetAudio()
if audioMessage == nil {
return errors.New("no audio has been passed")
}
audioMessage.Payload = payload
audioMessage.Type = audio.Type(payload)
m.Payload = &protobuf.ChatMessage_Audio{Audio: audioMessage}
return os.Remove(m.AudioPath)
}
func (m *Message) LoadImage() error {
payload, err := images.OpenAndAdjustImage(images.CroppedImage{ImagePath: m.ImagePath}, false)
if err != nil {
return err
}
imageMessage := m.GetImage()
imageMessage.Payload = payload
imageMessage.Format = images.GetProtobufImageFormat(payload)
m.Payload = &protobuf.ChatMessage_Image{Image: imageMessage}
return nil
}
func (m *Message) SetAlbumIDAndImagesCount(albumID string, imagesCount uint32) error {
imageMessage := m.GetImage()
if imageMessage == nil {
return errors.New("Image is empty")
}
imageMessage.AlbumId = albumID
imageMessage.AlbumImagesCount = imagesCount
m.Payload = &protobuf.ChatMessage_Image{Image: imageMessage}
return nil
}
func NewMessage() *Message {
return &Message{
ChatMessage: &protobuf.ChatMessage{},
}
}

View File

@@ -0,0 +1,498 @@
package common
import (
"fmt"
"net/url"
gethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol/protobuf"
)
type MakeMediaServerURLType func(msgID string, previewURL string, imageID MediaServerImageID) string
type MakeMediaServerURLMessageWrapperType func(previewURL string, imageID MediaServerImageID) string
type LinkPreviewThumbnail struct {
Width int `json:"width"`
Height int `json:"height"`
// Non-empty when the thumbnail is available via the media server, i.e. after
// the chat message is sent.
URL string `json:"url,omitempty"`
// Non-empty when the thumbnail payload needs to be shared with the client,
// but before it has been persisted.
DataURI string `json:"dataUri,omitempty"`
}
type LinkPreview struct {
Type protobuf.UnfurledLink_LinkType `json:"type"`
URL string `json:"url"`
Hostname string `json:"hostname"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Thumbnail LinkPreviewThumbnail `json:"thumbnail,omitempty"`
}
type StatusContactLinkPreview struct {
// PublicKey is: "0x" + hex-encoded decompressed public key.
// We keep it a string here for correct json marshalling.
PublicKey string `json:"publicKey"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Icon LinkPreviewThumbnail `json:"icon,omitempty"`
}
type StatusCommunityLinkPreview struct {
CommunityID string `json:"communityId"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
MembersCount uint32 `json:"membersCount"`
Color string `json:"color"`
Icon LinkPreviewThumbnail `json:"icon,omitempty"`
Banner LinkPreviewThumbnail `json:"banner,omitempty"`
}
type StatusCommunityChannelLinkPreview struct {
ChannelUUID string `json:"channelUuid"`
Emoji string `json:"emoji"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Color string `json:"color"`
Community *StatusCommunityLinkPreview `json:"community"`
}
type StatusLinkPreview struct {
URL string `json:"url,omitempty"`
Contact *StatusContactLinkPreview `json:"contact,omitempty"`
Community *StatusCommunityLinkPreview `json:"community,omitempty"`
Channel *StatusCommunityChannelLinkPreview `json:"channel,omitempty"`
}
func (thumbnail *LinkPreviewThumbnail) IsEmpty() bool {
return thumbnail.Width == 0 &&
thumbnail.Height == 0 &&
thumbnail.URL == "" &&
thumbnail.DataURI == ""
}
func (thumbnail *LinkPreviewThumbnail) clear() {
thumbnail.Width = 0
thumbnail.Height = 0
thumbnail.URL = ""
thumbnail.DataURI = ""
}
func (thumbnail *LinkPreviewThumbnail) validateForProto() error {
if thumbnail.DataURI == "" {
if thumbnail.Width == 0 && thumbnail.Height == 0 {
return nil
}
return fmt.Errorf("dataUri is empty, but width/height are not zero")
}
if thumbnail.Width == 0 || thumbnail.Height == 0 {
return fmt.Errorf("dataUri is not empty, but width/heigth are zero")
}
return nil
}
func (thumbnail *LinkPreviewThumbnail) convertToProto() (*protobuf.UnfurledLinkThumbnail, error) {
var payload []byte
var err error
if thumbnail.DataURI != "" {
payload, err = images.GetPayloadFromURI(thumbnail.DataURI)
if err != nil {
return nil, fmt.Errorf("could not get data URI payload, url='%s': %w", thumbnail.URL, err)
}
}
return &protobuf.UnfurledLinkThumbnail{
Width: uint32(thumbnail.Width),
Height: uint32(thumbnail.Height),
Payload: payload,
}, nil
}
func (thumbnail *LinkPreviewThumbnail) loadFromProto(
input *protobuf.UnfurledLinkThumbnail,
URL string,
imageID MediaServerImageID,
makeMediaServerURL MakeMediaServerURLMessageWrapperType) {
thumbnail.clear()
thumbnail.Width = int(input.Width)
thumbnail.Height = int(input.Height)
if len(input.Payload) > 0 {
thumbnail.URL = makeMediaServerURL(URL, imageID)
}
}
func (preview *LinkPreview) validateForProto() error {
switch preview.Type {
case protobuf.UnfurledLink_IMAGE:
if preview.URL == "" {
return fmt.Errorf("empty url")
}
if err := preview.Thumbnail.validateForProto(); err != nil {
return fmt.Errorf("thumbnail is not valid for proto: %w", err)
}
return nil
default: // Validate as a link type by default.
if preview.Title == "" {
return fmt.Errorf("title is empty")
}
if preview.URL == "" {
return fmt.Errorf("url is empty")
}
if err := preview.Thumbnail.validateForProto(); err != nil {
return fmt.Errorf("thumbnail is not valid for proto: %w", err)
}
return nil
}
}
func (preview *StatusLinkPreview) validateForProto() error {
if preview.URL == "" {
return fmt.Errorf("url can't be empty")
}
// At least and only one of Contact/Community/Channel should be present in the preview
if preview.Contact != nil && preview.Community != nil {
return fmt.Errorf("both contact and community are set at the same time")
}
if preview.Community != nil && preview.Channel != nil {
return fmt.Errorf("both community and channel are set at the same time")
}
if preview.Channel != nil && preview.Contact != nil {
return fmt.Errorf("both contact and channel are set at the same time")
}
if preview.Contact == nil && preview.Community == nil && preview.Channel == nil {
return fmt.Errorf("none of contact/community/channel are set")
}
if preview.Contact != nil {
if preview.Contact.PublicKey == "" {
return fmt.Errorf("contact publicKey is empty")
}
if err := preview.Contact.Icon.validateForProto(); err != nil {
return fmt.Errorf("contact icon invalid: %w", err)
}
return nil
}
if preview.Community != nil {
return preview.Community.validateForProto()
}
if preview.Channel != nil {
if preview.Channel.ChannelUUID == "" {
return fmt.Errorf("channelUuid is empty")
}
if preview.Channel.Community == nil {
return fmt.Errorf("channel community is nil")
}
if err := preview.Channel.Community.validateForProto(); err != nil {
return fmt.Errorf("channel community is not valid: %w", err)
}
return nil
}
return nil
}
func (preview *StatusCommunityLinkPreview) validateForProto() error {
if preview == nil {
return fmt.Errorf("community preview is empty")
}
if preview.CommunityID == "" {
return fmt.Errorf("communityId is empty")
}
if err := preview.Icon.validateForProto(); err != nil {
return fmt.Errorf("community icon is invalid: %w", err)
}
if err := preview.Banner.validateForProto(); err != nil {
return fmt.Errorf("community banner is invalid: %w", err)
}
return nil
}
func (preview *StatusCommunityLinkPreview) convertToProto() (*protobuf.UnfurledStatusCommunityLink, error) {
if preview == nil {
return nil, nil
}
icon, err := preview.Icon.convertToProto()
if err != nil {
return nil, err
}
banner, err := preview.Banner.convertToProto()
if err != nil {
return nil, err
}
communityID, err := types.DecodeHex(preview.CommunityID)
if err != nil {
return nil, fmt.Errorf("failed to decode community id: %w", err)
}
community := &protobuf.UnfurledStatusCommunityLink{
CommunityId: communityID,
DisplayName: preview.DisplayName,
Description: preview.Description,
MembersCount: preview.MembersCount,
Color: preview.Color,
Icon: icon,
Banner: banner,
}
return community, nil
}
func (preview *StatusCommunityLinkPreview) loadFromProto(c *protobuf.UnfurledStatusCommunityLink,
URL string, thumbnailPrefix MediaServerImageIDPrefix,
makeMediaServerURL MakeMediaServerURLMessageWrapperType) {
preview.CommunityID = types.EncodeHex(c.CommunityId)
preview.DisplayName = c.DisplayName
preview.Description = c.Description
preview.MembersCount = c.MembersCount
preview.Color = c.Color
preview.Icon.clear()
preview.Banner.clear()
if icon := c.GetIcon(); icon != nil {
preview.Icon.loadFromProto(icon, URL, CreateImageID(thumbnailPrefix, MediaServerIconPostfix), makeMediaServerURL)
}
if banner := c.GetBanner(); banner != nil {
preview.Banner.loadFromProto(banner, URL, CreateImageID(thumbnailPrefix, MediaServerBannerPostfix), makeMediaServerURL)
}
}
// ConvertLinkPreviewsToProto expects previews to be correctly sent by the
// client because we can't attempt to re-unfurl URLs at this point (it's
// actually undesirable). We run a basic validation as an additional safety net.
func (m *Message) ConvertLinkPreviewsToProto() ([]*protobuf.UnfurledLink, error) {
if len(m.LinkPreviews) == 0 {
return nil, nil
}
unfurledLinks := make([]*protobuf.UnfurledLink, 0, len(m.LinkPreviews))
for _, preview := range m.LinkPreviews {
// Do not process subsequent previews because we do expect all previews to
// be valid at this stage.
if err := preview.validateForProto(); err != nil {
return nil, fmt.Errorf("invalid link preview, url='%s': %w", preview.URL, err)
}
var payload []byte
var err error
if preview.Thumbnail.DataURI != "" {
payload, err = images.GetPayloadFromURI(preview.Thumbnail.DataURI)
if err != nil {
return nil, fmt.Errorf("could not get data URI payload, url='%s': %w", preview.URL, err)
}
}
ul := &protobuf.UnfurledLink{
Type: preview.Type,
Url: preview.URL,
Title: preview.Title,
Description: preview.Description,
ThumbnailWidth: uint32(preview.Thumbnail.Width),
ThumbnailHeight: uint32(preview.Thumbnail.Height),
ThumbnailPayload: payload,
}
unfurledLinks = append(unfurledLinks, ul)
}
return unfurledLinks, nil
}
func (m *Message) ConvertFromProtoToLinkPreviews(makeMediaServerURL func(msgID string, previewURL string) string) []LinkPreview {
var links []*protobuf.UnfurledLink
if links = m.GetUnfurledLinks(); links == nil {
return nil
}
previews := make([]LinkPreview, 0, len(links))
for _, link := range links {
parsedURL, err := url.Parse(link.Url)
var hostname string
// URL parsing in Go can fail with URLs that weren't correctly URL encoded.
// This shouldn't happen in general, but if an error happens we just reuse
// the full URL.
if err != nil {
hostname = link.Url
} else {
hostname = parsedURL.Hostname()
}
lp := LinkPreview{
Description: link.Description,
Hostname: hostname,
Title: link.Title,
Type: link.Type,
URL: link.Url,
}
mediaURL := ""
if len(link.ThumbnailPayload) > 0 {
mediaURL = makeMediaServerURL(m.ID, link.Url)
}
if link.GetThumbnailPayload() != nil {
lp.Thumbnail.Width = int(link.ThumbnailWidth)
lp.Thumbnail.Height = int(link.ThumbnailHeight)
lp.Thumbnail.URL = mediaURL
}
previews = append(previews, lp)
}
return previews
}
func (m *Message) ConvertStatusLinkPreviewsToProto() (*protobuf.UnfurledStatusLinks, error) {
if len(m.StatusLinkPreviews) == 0 {
return nil, nil
}
unfurledLinks := make([]*protobuf.UnfurledStatusLink, 0, len(m.StatusLinkPreviews))
for _, preview := range m.StatusLinkPreviews {
// We expect all previews to be valid at this stage
if err := preview.validateForProto(); err != nil {
return nil, fmt.Errorf("invalid status link preview, url='%s': %w", preview.URL, err)
}
ul := &protobuf.UnfurledStatusLink{
Url: preview.URL,
}
if preview.Contact != nil {
decompressedPublicKey, err := types.DecodeHex(preview.Contact.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decode contact public key: %w", err)
}
publicKey, err := crypto.UnmarshalPubkey(decompressedPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal decompressed public key: %w", err)
}
compressedPublicKey := crypto.CompressPubkey(publicKey)
icon, err := preview.Contact.Icon.convertToProto()
if err != nil {
return nil, err
}
ul.Payload = &protobuf.UnfurledStatusLink_Contact{
Contact: &protobuf.UnfurledStatusContactLink{
PublicKey: compressedPublicKey,
DisplayName: preview.Contact.DisplayName,
Description: preview.Contact.Description,
Icon: icon,
},
}
}
if preview.Community != nil {
communityPreview, err := preview.Community.convertToProto()
if err != nil {
return nil, err
}
ul.Payload = &protobuf.UnfurledStatusLink_Community{
Community: communityPreview,
}
}
if preview.Channel != nil {
communityPreview, err := preview.Channel.Community.convertToProto()
if err != nil {
return nil, err
}
ul.Payload = &protobuf.UnfurledStatusLink_Channel{
Channel: &protobuf.UnfurledStatusChannelLink{
ChannelUuid: preview.Channel.ChannelUUID,
Emoji: preview.Channel.Emoji,
DisplayName: preview.Channel.DisplayName,
Description: preview.Channel.Description,
Color: preview.Channel.Color,
Community: communityPreview,
},
}
}
unfurledLinks = append(unfurledLinks, ul)
}
return &protobuf.UnfurledStatusLinks{UnfurledStatusLinks: unfurledLinks}, nil
}
func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(msgID string, previewURL string, imageID MediaServerImageID) string) []StatusLinkPreview {
if m.GetUnfurledStatusLinks() == nil {
return nil
}
links := m.UnfurledStatusLinks.GetUnfurledStatusLinks()
if links == nil {
return nil
}
// This wrapper adds the messageID to the callback
makeMediaServerURLMessageWrapper := func(previewURL string, imageID MediaServerImageID) string {
return makeMediaServerURL(m.ID, previewURL, imageID)
}
previews := make([]StatusLinkPreview, 0, len(links))
for _, link := range links {
lp := StatusLinkPreview{
URL: link.Url,
}
if c := link.GetContact(); c != nil {
publicKey, err := crypto.DecompressPubkey(c.PublicKey)
if err != nil {
continue
}
lp.Contact = &StatusContactLinkPreview{
PublicKey: types.EncodeHex(gethcrypto.FromECDSAPub(publicKey)),
DisplayName: c.DisplayName,
Description: c.Description,
}
if icon := c.GetIcon(); icon != nil {
lp.Contact.Icon.loadFromProto(icon, link.Url, MediaServerContactIcon, makeMediaServerURLMessageWrapper)
}
}
if c := link.GetCommunity(); c != nil {
lp.Community = new(StatusCommunityLinkPreview)
lp.Community.loadFromProto(c, link.Url, MediaServerCommunityPrefix, makeMediaServerURLMessageWrapper)
}
if c := link.GetChannel(); c != nil {
lp.Channel = &StatusCommunityChannelLinkPreview{
ChannelUUID: c.ChannelUuid,
Emoji: c.Emoji,
DisplayName: c.DisplayName,
Description: c.Description,
Color: c.Color,
}
if c.Community != nil {
lp.Channel.Community = new(StatusCommunityLinkPreview)
lp.Channel.Community.loadFromProto(c.Community, link.Url, MediaServerChannelCommunityPrefix, makeMediaServerURLMessageWrapper)
}
}
previews = append(previews, lp)
}
return previews
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
package common
import (
"crypto/ecdsa"
"github.com/golang/protobuf/proto"
"github.com/status-im/status-go/protocol/protobuf"
)
type PinnedMessages []*PinnedMessage
func (m PinnedMessages) GetClock(i int) uint64 {
return m[i].Message.Clock
}
type PinMessage struct {
*protobuf.PinMessage
// ID calculated as keccak256(compressedAuthorPubKey, data) where data is unencrypted payload.
ID string `json:"id"`
// MessageID string `json:"messageID"`
// WhisperTimestamp is a timestamp of a Whisper envelope.
WhisperTimestamp uint64 `json:"whisperTimestamp"`
// From is a public key of the user who pinned the message.
From string `json:"from"`
// The chat id to be stored locally
LocalChatID string `json:"localChatId"`
SigPubKey *ecdsa.PublicKey `json:"-"`
// Identicon of the author
Identicon string `json:"identicon"`
// Random 3 words name
Alias string `json:"alias"`
Message *PinnedMessage `json:"pinnedMessage"`
}
func NewPinMessage() *PinMessage {
return &PinMessage{PinMessage: &protobuf.PinMessage{}}
}
type PinnedMessage struct {
Message *Message `json:"message"`
PinnedAt uint64 `json:"pinnedAt"`
PinnedBy string `json:"pinnedBy"`
}
// WrapGroupMessage indicates whether we should wrap this in membership information
func (m *PinMessage) WrapGroupMessage() bool {
return false
}
// SetMessageType a setter for the MessageType field
// this function is required to implement the ChatEntity interface
func (m *PinMessage) SetMessageType(messageType protobuf.MessageType) {
m.MessageType = messageType
}
// GetProtoBuf returns the struct's embedded protobuf struct
// this function is required to implement the ChatEntity interface
func (m *PinMessage) GetProtobuf() proto.Message {
return m.PinMessage
}
// GetSigPubKey returns an ecdsa encoded public key
// this function is required to implement the ChatEntity interface
func (m *PinMessage) GetSigPubKey() *ecdsa.PublicKey {
return m.SigPubKey
}

View File

@@ -0,0 +1,41 @@
package common
import (
"crypto/ecdsa"
"github.com/status-im/status-go/protocol/protobuf"
)
type CommKeyExMsgType uint8
const (
KeyExMsgNone CommKeyExMsgType = 0
KeyExMsgReuse CommKeyExMsgType = 1
KeyExMsgRekey CommKeyExMsgType = 2
)
// RawMessage represent a sent or received message, kept for being able
// to re-send/propagate
type RawMessage struct {
ID string
LocalChatID string
LastSent uint64
SendCount int
Sent bool
ResendAutomatically bool
SkipEncryptionLayer bool // don't wrap message into ProtocolMessage
SendPushNotification bool
MessageType protobuf.ApplicationMetadataMessage_Type
Payload []byte
Sender *ecdsa.PrivateKey
Recipients []*ecdsa.PublicKey
SkipGroupMessageWrap bool
SkipApplicationWrap bool
SendOnPersonalTopic bool
CommunityID []byte
CommunityKeyExMsgType CommKeyExMsgType
Ephemeral bool
BeforeDispatch func(*RawMessage) error
HashRatchetGroupID []byte
PubsubTopic string
}

View File

@@ -0,0 +1,424 @@
package common
import (
"bytes"
"context"
"crypto/ecdsa"
"database/sql"
"encoding/gob"
"strings"
"time"
"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/protobuf"
)
type RawMessageConfirmation struct {
// DataSyncID is the ID of the datasync message sent
DataSyncID []byte
// MessageID is the message id of the message
MessageID []byte
// PublicKey is the compressed receiver public key
PublicKey []byte
// ConfirmedAt is the unix timestamp in seconds of when the message was confirmed
ConfirmedAt int64
}
type RawMessagesPersistence struct {
db *sql.DB
}
func NewRawMessagesPersistence(db *sql.DB) *RawMessagesPersistence {
return &RawMessagesPersistence{db: db}
}
func (db RawMessagesPersistence) SaveRawMessage(message *RawMessage) error {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
var pubKeys [][]byte
for _, pk := range message.Recipients {
pubKeys = append(pubKeys, crypto.CompressPubkey(pk))
}
// Encode recipients
var encodedRecipients bytes.Buffer
encoder := gob.NewEncoder(&encodedRecipients)
if err := encoder.Encode(pubKeys); err != nil {
return err
}
// If the message is not sent, we check whether there's a record
// in the database already and preserve the state
if !message.Sent {
oldMessage, err := db.rawMessageByID(tx, message.ID)
if err != nil && err != sql.ErrNoRows {
return err
}
if oldMessage != nil {
message.Sent = oldMessage.Sent
}
}
_, err = tx.Exec(`
INSERT INTO
raw_messages
(
id,
local_chat_id,
last_sent,
send_count,
sent,
message_type,
resend_automatically,
recipients,
skip_encryption,
send_push_notification,
skip_group_message_wrap,
send_on_personal_topic,
payload
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
message.ID,
message.LocalChatID,
message.LastSent,
message.SendCount,
message.Sent,
message.MessageType,
message.ResendAutomatically,
encodedRecipients.Bytes(),
message.SkipEncryptionLayer,
message.SendPushNotification,
message.SkipGroupMessageWrap,
message.SendOnPersonalTopic,
message.Payload)
return err
}
func (db RawMessagesPersistence) RawMessageByID(id string) (*RawMessage, error) {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return nil, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
return db.rawMessageByID(tx, id)
}
func (db RawMessagesPersistence) rawMessageByID(tx *sql.Tx, id string) (*RawMessage, error) {
var rawPubKeys [][]byte
var encodedRecipients []byte
var skipGroupMessageWrap sql.NullBool
var sendOnPersonalTopic sql.NullBool
message := &RawMessage{}
err := tx.QueryRow(`
SELECT
id,
local_chat_id,
last_sent,
send_count,
sent,
message_type,
resend_automatically,
recipients,
skip_encryption,
send_push_notification,
skip_group_message_wrap,
send_on_personal_topic,
payload
FROM
raw_messages
WHERE
id = ?`,
id,
).Scan(
&message.ID,
&message.LocalChatID,
&message.LastSent,
&message.SendCount,
&message.Sent,
&message.MessageType,
&message.ResendAutomatically,
&encodedRecipients,
&message.SkipEncryptionLayer,
&message.SendPushNotification,
&skipGroupMessageWrap,
&sendOnPersonalTopic,
&message.Payload,
)
if err != nil {
return nil, err
}
if rawPubKeys != nil {
// Restore recipients
decoder := gob.NewDecoder(bytes.NewBuffer(encodedRecipients))
err = decoder.Decode(&rawPubKeys)
if err != nil {
return nil, err
}
for _, pkBytes := range rawPubKeys {
pubkey, err := crypto.UnmarshalPubkey(pkBytes)
if err != nil {
return nil, err
}
message.Recipients = append(message.Recipients, pubkey)
}
}
if skipGroupMessageWrap.Valid {
message.SkipGroupMessageWrap = skipGroupMessageWrap.Bool
}
if sendOnPersonalTopic.Valid {
message.SendOnPersonalTopic = sendOnPersonalTopic.Bool
}
return message, nil
}
func (db RawMessagesPersistence) RawMessagesIDsByType(t protobuf.ApplicationMetadataMessage_Type) ([]string, error) {
ids := []string{}
rows, err := db.db.Query(`
SELECT
id
FROM
raw_messages
WHERE
message_type = ?`,
t)
if err != nil {
return ids, err
}
defer rows.Close()
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return ids, err
}
ids = append(ids, id)
}
return ids, nil
}
// MarkAsConfirmed marks all the messages with dataSyncID as confirmed and returns
// the messageIDs that can be considered confirmed.
// If atLeastOne is set it will return messageid if at least once of the messages
// sent has been confirmed
func (db RawMessagesPersistence) MarkAsConfirmed(dataSyncID []byte, atLeastOne bool) (messageID types.HexBytes, err error) {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return nil, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
confirmedAt := time.Now().Unix()
_, err = tx.Exec(`UPDATE raw_message_confirmations SET confirmed_at = ? WHERE datasync_id = ? AND confirmed_at = 0`, confirmedAt, dataSyncID)
if err != nil {
return
}
// Select any tuple that has a message_id with a datasync_id = ? and that has just been confirmed
rows, err := tx.Query(`SELECT message_id,confirmed_at FROM raw_message_confirmations WHERE message_id = (SELECT message_id FROM raw_message_confirmations WHERE datasync_id = ? LIMIT 1)`, dataSyncID)
if err != nil {
return
}
defer rows.Close()
confirmedResult := true
for rows.Next() {
var confirmedAt int64
err = rows.Scan(&messageID, &confirmedAt)
if err != nil {
return
}
confirmed := confirmedAt > 0
if atLeastOne && confirmed {
// We return, as at least one was confirmed
return
}
confirmedResult = confirmedResult && confirmed
}
if !confirmedResult {
messageID = nil
return
}
return
}
func (db RawMessagesPersistence) InsertPendingConfirmation(confirmation *RawMessageConfirmation) error {
_, err := db.db.Exec(`INSERT INTO raw_message_confirmations
(datasync_id, message_id, public_key)
VALUES
(?,?,?)`,
confirmation.DataSyncID,
confirmation.MessageID,
confirmation.PublicKey,
)
return err
}
func (db RawMessagesPersistence) SaveHashRatchetMessage(groupID []byte, keyID []byte, m *types.Message) error {
_, err := db.db.Exec(`INSERT INTO hash_ratchet_encrypted_messages(hash, sig, TTL, timestamp, topic, payload, dst, p2p, padding, group_id, key_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, m.Hash, m.Sig, m.TTL, m.Timestamp, types.TopicTypeToByteArray(m.Topic), m.Payload, m.Dst, m.P2P, m.Padding, groupID, keyID)
return err
}
func (db RawMessagesPersistence) GetHashRatchetMessages(keyID []byte) ([]*types.Message, error) {
var messages []*types.Message
rows, err := db.db.Query(`SELECT hash, sig, TTL, timestamp, topic, payload, dst, p2p, padding FROM hash_ratchet_encrypted_messages WHERE key_id = ?`, keyID)
if err != nil {
return nil, err
}
for rows.Next() {
var topic []byte
message := &types.Message{}
err := rows.Scan(&message.Hash, &message.Sig, &message.TTL, &message.Timestamp, &topic, &message.Payload, &message.Dst, &message.P2P, &message.Padding)
if err != nil {
return nil, err
}
message.Topic = types.BytesToTopic(topic)
messages = append(messages, message)
}
return messages, nil
}
func (db RawMessagesPersistence) DeleteHashRatchetMessages(ids [][]byte) error {
if len(ids) == 0 {
return nil
}
idsArgs := make([]interface{}, 0, len(ids))
for _, id := range ids {
idsArgs = append(idsArgs, id)
}
inVector := strings.Repeat("?, ", len(ids)-1) + "?"
_, err := db.db.Exec("DELETE FROM hash_ratchet_encrypted_messages WHERE hash IN ("+inVector+")", idsArgs...) // nolint: gosec
return err
}
func (db *RawMessagesPersistence) IsMessageAlreadyCompleted(hash []byte) (bool, error) {
var alreadyCompleted int
err := db.db.QueryRow("SELECT COUNT(*) FROM message_segments_completed WHERE hash = ?", hash).Scan(&alreadyCompleted)
if err != nil {
return false, err
}
return alreadyCompleted > 0, nil
}
func (db *RawMessagesPersistence) SaveMessageSegment(segment *protobuf.SegmentMessage, sigPubKey *ecdsa.PublicKey, timestamp int64) error {
sigPubKeyBlob := crypto.CompressPubkey(sigPubKey)
_, err := db.db.Exec("INSERT INTO message_segments (hash, segment_index, segments_count, sig_pub_key, payload, timestamp) VALUES (?, ?, ?, ?, ?, ?)",
segment.EntireMessageHash, segment.Index, segment.SegmentsCount, sigPubKeyBlob, segment.Payload, timestamp)
return err
}
// Get ordered message segments for given hash
func (db *RawMessagesPersistence) GetMessageSegments(hash []byte, sigPubKey *ecdsa.PublicKey) ([]*protobuf.SegmentMessage, error) {
sigPubKeyBlob := crypto.CompressPubkey(sigPubKey)
rows, err := db.db.Query("SELECT hash, segment_index, segments_count, payload FROM message_segments WHERE hash = ? AND sig_pub_key = ? ORDER BY segment_index", hash, sigPubKeyBlob)
if err != nil {
return nil, err
}
defer rows.Close()
var segments []*protobuf.SegmentMessage
for rows.Next() {
var segment protobuf.SegmentMessage
err := rows.Scan(&segment.EntireMessageHash, &segment.Index, &segment.SegmentsCount, &segment.Payload)
if err != nil {
return nil, err
}
segments = append(segments, &segment)
}
err = rows.Err()
if err != nil {
return nil, err
}
return segments, nil
}
func (db *RawMessagesPersistence) RemoveMessageSegmentsOlderThan(timestamp int64) error {
_, err := db.db.Exec("DELETE FROM message_segments WHERE timestamp < ?", timestamp)
return err
}
func (db *RawMessagesPersistence) CompleteMessageSegments(hash []byte, sigPubKey *ecdsa.PublicKey, timestamp int64) error {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
sigPubKeyBlob := crypto.CompressPubkey(sigPubKey)
_, err = tx.Exec("DELETE FROM message_segments WHERE hash = ? AND sig_pub_key = ?", hash, sigPubKeyBlob)
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO message_segments_completed (hash, sig_pub_key, timestamp) VALUES (?,?,?)", hash, sigPubKeyBlob, timestamp)
if err != nil {
return err
}
return err
}
func (db *RawMessagesPersistence) RemoveMessageSegmentsCompletedOlderThan(timestamp int64) error {
_, err := db.db.Exec("DELETE FROM message_segments_completed WHERE timestamp < ?", timestamp)
return err
}

View File

@@ -0,0 +1,63 @@
package shard
import (
wakuproto "github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/status-im/status-go/protocol/protobuf"
)
type Shard struct {
Cluster uint16 `json:"cluster"`
Index uint16 `json:"index"`
}
func FromProtobuff(p *protobuf.Shard) *Shard {
if p == nil {
return nil
}
return &Shard{
Cluster: uint16(p.Cluster),
Index: uint16(p.Index),
}
}
func (s *Shard) Protobuffer() *protobuf.Shard {
if s == nil {
return nil
}
return &protobuf.Shard{
Cluster: int32(s.Cluster),
Index: int32(s.Index),
}
}
func (s *Shard) PubsubTopic() string {
if s != nil {
return wakuproto.NewStaticShardingPubsubTopic(s.Cluster, s.Index).String()
}
return ""
}
func DefaultNonProtectedPubsubTopic() string {
return (&Shard{
Cluster: MainStatusShardCluster,
Index: NonProtectedShardIndex,
}).PubsubTopic()
}
const MainStatusShardCluster = 16
const DefaultShardIndex = 32
const NonProtectedShardIndex = 64
const UndefinedShardValue = 0
func DefaultShardPubsubTopic() string {
return wakuproto.NewStaticShardingPubsubTopic(MainStatusShardCluster, DefaultShardIndex).String()
}
func DefaultShard() *Shard {
return &Shard{
Cluster: MainStatusShardCluster,
Index: NonProtectedShardIndex,
}
}

View File

@@ -0,0 +1,12 @@
package common
// TimeSource provides a unified way of getting the current time.
// The intention is to always use a synchronized time source
// between all components of the protocol.
//
// This is required by Whisper and Waku protocols
// which rely on a fact that all peers
// have a synchronized time source.
type TimeSource interface {
GetCurrentTime() uint64
}