420 lines
16 KiB
Go
420 lines
16 KiB
Go
|
|
// sendMessages provides functions to create and send messages
|
|
|
|
package sendMessages
|
|
|
|
//TODO: Add function to prune mySentMessagesMapDatastore
|
|
|
|
import "seekia/internal/convertCurrencies"
|
|
import "seekia/internal/cryptography/kyber"
|
|
import "seekia/internal/cryptography/nacl"
|
|
import "seekia/internal/encoding"
|
|
import "seekia/internal/helpers"
|
|
import "seekia/internal/identity"
|
|
import "seekia/internal/logger"
|
|
import "seekia/internal/messaging/createMessages"
|
|
import "seekia/internal/messaging/inbox"
|
|
import "seekia/internal/messaging/myChatKeys"
|
|
import "seekia/internal/messaging/myChatMessages"
|
|
import "seekia/internal/messaging/mySecretInboxes"
|
|
import "seekia/internal/messaging/peerChatKeys"
|
|
import "seekia/internal/messaging/peerSecretInboxes"
|
|
import "seekia/internal/messaging/readMessages"
|
|
import "seekia/internal/myDatastores/myMap"
|
|
import "seekia/internal/myDevice"
|
|
import "seekia/internal/myIdentity"
|
|
import "seekia/internal/network/myBroadcasts"
|
|
import "seekia/internal/network/spendCredit"
|
|
import "seekia/internal/parameters/getParameters"
|
|
import "seekia/internal/profiles/myProfileStatus"
|
|
|
|
import "time"
|
|
import "errors"
|
|
import "sync"
|
|
|
|
// This mutex will be locked whenever we are adding a new message to send
|
|
var addingMessageMutex sync.Mutex
|
|
|
|
// This list host a map of messages which we have already sent
|
|
// Map Structure: Message Identifier -> Message Hash
|
|
var mySentMessagesMapDatastore *myMap.MyMap
|
|
|
|
// This function must be called whenever an app user signs in
|
|
func InitializeSentMessagesDatastore()error{
|
|
|
|
addingMessageMutex.Lock()
|
|
defer addingMessageMutex.Unlock()
|
|
|
|
newMySentMessagesMapDatastore, err := myMap.CreateNewMap("SentMessages")
|
|
if (err != nil) { return err }
|
|
|
|
mySentMessagesMapDatastore = newMySentMessagesMapDatastore
|
|
|
|
return nil
|
|
}
|
|
|
|
func WaitForPendingSendsToComplete(){
|
|
|
|
pendingMessagesWaitgroup.Wait()
|
|
}
|
|
|
|
// This waitgroup will be used to wait for all pending messages to be sent
|
|
var pendingMessagesWaitgroup sync.WaitGroup
|
|
|
|
var pendingMessagesMapMutex sync.RWMutex
|
|
|
|
// This list contains a map of message identifiers which we are currently attempting to send
|
|
var pendingMessagesMap map[[20]byte]struct{} = make(map[[20]byte]struct{})
|
|
|
|
func addMessageToPendingMessagesMap(messageIdentifier [20]byte){
|
|
pendingMessagesMapMutex.Lock()
|
|
pendingMessagesMap[messageIdentifier] = struct{}{}
|
|
pendingMessagesMapMutex.Unlock()
|
|
}
|
|
|
|
func deleteMessageFromPendingMessagesMap(messageIdentifier [20]byte){
|
|
|
|
pendingMessagesMapMutex.Lock()
|
|
delete(pendingMessagesMap, messageIdentifier)
|
|
pendingMessagesMapMutex.Unlock()
|
|
}
|
|
|
|
func checkIfMessageIsPending(messageIdentifier [20]byte)bool{
|
|
|
|
pendingMessagesMapMutex.RLock()
|
|
_, exists := pendingMessagesMap[messageIdentifier]
|
|
pendingMessagesMapMutex.RUnlock()
|
|
if (exists == false){
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// This function will tell us if a message has been successfully sent
|
|
//Outputs:
|
|
// -bool: Message is sent
|
|
// -[26]byte: Message hash
|
|
// -error
|
|
func CheckIfMessageIsSent(messageIdentifier [20]byte)(bool, [26]byte, error){
|
|
|
|
messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:])
|
|
|
|
exists, messageHashString, err := mySentMessagesMapDatastore.GetMapEntry(messageIdentifierHex)
|
|
if (err != nil) { return false, [26]byte{}, err }
|
|
if (exists == false){
|
|
return false, [26]byte{}, nil
|
|
}
|
|
|
|
messageHash, err := readMessages.ReadMessageHashHex(messageHashString)
|
|
if (err != nil){
|
|
return false, [26]byte{}, errors.New("mySentMessagesMapDatastore contains invalid map value: Not hex: " + messageHashString)
|
|
}
|
|
|
|
return true, messageHash, nil
|
|
}
|
|
|
|
//Outputs:
|
|
// -bool: Message is pending or sent (we are already trying to send the message, or we already sent it)
|
|
// -bool: Successfully started send (required information exists)
|
|
// -error
|
|
func StartSendingMessage(messageIdentifier [20]byte, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageNetworkType byte, messageTimeSent int64, messageCommunication string, messageDuration int)(bool, bool, error){
|
|
|
|
isValid := helpers.VerifyNetworkType(messageNetworkType)
|
|
if (isValid == false){
|
|
messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
|
|
return false, false, errors.New("StartSendingMessage called with invalid messageNetworkType: " + messageNetworkTypeString)
|
|
}
|
|
|
|
addingMessageMutex.Lock()
|
|
defer addingMessageMutex.Unlock()
|
|
|
|
messageIsPending := checkIfMessageIsPending(messageIdentifier)
|
|
if (messageIsPending == true){
|
|
// We are already trying to send this message.
|
|
return true, false, nil
|
|
}
|
|
|
|
messageIsSent, _, err := CheckIfMessageIsSent(messageIdentifier)
|
|
if (err != nil) { return false, false, err }
|
|
if (messageIsSent == true){
|
|
// We already sent the message
|
|
return true, false, nil
|
|
}
|
|
|
|
identityExists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeysFromIdentityHash(myIdentityHash)
|
|
if (err != nil) { return false, false, err }
|
|
if (identityExists == false) {
|
|
return false, false, errors.New("StartSendingMessage called when my identity does not exist.")
|
|
}
|
|
|
|
myIdentityExists, isActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, messageNetworkType)
|
|
if (err != nil) { return false, false, err }
|
|
if (myIdentityExists == false) {
|
|
return false, false, errors.New("My identity not found after being found already.")
|
|
}
|
|
if (isActiveStatus == false){
|
|
return false, false, nil
|
|
}
|
|
|
|
exists, myChatKeysLatestUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, messageNetworkType)
|
|
if (err != nil) { return false, false, err }
|
|
if (exists == false){
|
|
// We have not broadcast a profile yet.
|
|
return false, false, nil
|
|
}
|
|
|
|
//Outputs:
|
|
// -[10]byte: Recipient Inbox
|
|
// -[32]byte: Recipient inbox sealer key
|
|
// -error
|
|
getRecipientInbox := func()([10]byte, [32]byte, error){
|
|
|
|
secretInboxFound, theirSecretInbox, theirSecretInboxSealerKey, err := peerSecretInboxes.GetPeerConversationCurrentSecretInbox(myIdentityHash, recipientIdentityHash, messageNetworkType)
|
|
if (err != nil) { return [10]byte{}, [32]byte{}, err }
|
|
if (secretInboxFound == true){
|
|
return theirSecretInbox, theirSecretInboxSealerKey, nil
|
|
}
|
|
|
|
recipientPublicInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash)
|
|
if (err != nil) { return [10]byte{}, [32]byte{}, err }
|
|
|
|
recipientPublicInboxSealerKey, err := inbox.GetPublicInboxSealerKeyFromIdentityHash(recipientIdentityHash)
|
|
if (err != nil) { return [10]byte{}, [32]byte{}, err }
|
|
|
|
return recipientPublicInbox, recipientPublicInboxSealerKey, nil
|
|
}
|
|
|
|
recipientInbox, recipientInboxSealerKey, err := getRecipientInbox()
|
|
if (err != nil) { return false, false, err }
|
|
|
|
if (len(recipientInboxSealerKey) != 32){
|
|
return false, false, errors.New("getRecipientInbox returning invalid recipientInboxSealerKey: Invalid length.")
|
|
}
|
|
|
|
recipientInboxSealerKeyArray := [32]byte(recipientInboxSealerKey)
|
|
|
|
peerIsDisabled, peerChatKeysExist, recipientNaclKey, recipientKyberKey, err := peerChatKeys.GetPeerNewestActiveChatKeys(recipientIdentityHash, messageNetworkType)
|
|
if (err != nil) { return false, false, err }
|
|
if (peerIsDisabled == true || peerChatKeysExist == false) {
|
|
return false, false, nil
|
|
}
|
|
|
|
// The secret inboxes returned from the function below may not exist yet in our storage.
|
|
// If they are missing, we will create new secret inbox(es)
|
|
// We will add our new secret inboxes to our mySecretInboxes storage only if the message send is successful
|
|
|
|
parametersExist, myCurrentSecretInboxExists, myExistingCurrentSecretInboxSeed, myNextSecretInboxExists, myExistingNextSecretInboxSeed, err := mySecretInboxes.GetMySecretInboxSeedsForMessage(myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent)
|
|
if (err != nil) { return false, false, err }
|
|
if (parametersExist == false){
|
|
// We cannot determine the current epoch without the epoch parameters
|
|
return false, false, nil
|
|
}
|
|
|
|
getMyCurrentSecretInboxSeed := func()([22]byte, error){
|
|
|
|
if (myCurrentSecretInboxExists == true){
|
|
return myExistingCurrentSecretInboxSeed, nil
|
|
}
|
|
|
|
newCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
|
|
if (err != nil) { return [22]byte{}, err }
|
|
|
|
return newCurrentSecretInboxSeed, nil
|
|
}
|
|
|
|
getMyNextSecretInboxSeed := func()([22]byte, error){
|
|
|
|
if (myNextSecretInboxExists == true){
|
|
return myExistingNextSecretInboxSeed, nil
|
|
}
|
|
|
|
newNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
|
|
if (err != nil) { return [22]byte{}, err }
|
|
|
|
return newNextSecretInboxSeed, nil
|
|
}
|
|
|
|
myCurrentSecretInboxSeed, err := getMyCurrentSecretInboxSeed()
|
|
if (err != nil) { return false, false, err }
|
|
|
|
myNextSecretInboxSeed, err := getMyNextSecretInboxSeed()
|
|
if (err != nil) { return false, false, err }
|
|
|
|
myIdentityFound, myDeviceIdentifier, err := myDevice.GetMyDeviceIdentifier(myIdentityHash, messageNetworkType)
|
|
if (err != nil) { return false, false, err }
|
|
if (myIdentityFound == false){
|
|
return false, false, errors.New("My identity not found after being found already.")
|
|
}
|
|
|
|
chatParametersExist, err := getParameters.CheckIfChatParametersExist(messageNetworkType)
|
|
if (err != nil) { return false, false, err }
|
|
if (chatParametersExist == false){
|
|
return false, false, nil
|
|
}
|
|
|
|
// We have everything we need to send the message
|
|
// It will only fail in 2 ways:
|
|
// 1. We cannot connect to the account credit server(s)
|
|
// 2. The cost of sending messages has increased since we froze the funds, and we don't have enough funds to send the message anymore
|
|
|
|
finalMessage, messageHash, err := createMessages.CreateChatMessage(messageNetworkType, myIdentityHash, myIdentityPublicKey, myIdentityPrivateKey, messageTimeSent, myCurrentSecretInboxSeed, myNextSecretInboxSeed, myDeviceIdentifier, myChatKeysLatestUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclKey, recipientKyberKey, recipientInboxSealerKeyArray)
|
|
if (err != nil) { return false, false, err }
|
|
|
|
sendMessageFunction := func(){
|
|
|
|
attemptToSendMessage := func()error{
|
|
|
|
messageSize := len(finalMessage)
|
|
|
|
sufficientFundsExist, successfullyFunded, err := spendCredit.FundMyMessageWithServer(messageIdentifier, messageHash, messageNetworkType, messageSize, messageDuration)
|
|
if (err != nil) { return err }
|
|
if (sufficientFundsExist == false){
|
|
// Message send is not successful because we ran out of funds.
|
|
// We will try to send message again in the future, when hopefully user has added more funds
|
|
// This should only happen if the cost of sending the message has gone up since user froze funds
|
|
return nil
|
|
}
|
|
if (successfullyFunded == false){
|
|
// We could not connect to the server
|
|
return nil
|
|
}
|
|
|
|
// Message was successfully funded
|
|
// We add it to our broadcasts to be automatically broadcast
|
|
// We also add the secret inbox seeds and the message map to our chat messages
|
|
|
|
parametersExist, err := mySecretInboxes.AddMySecretInboxSeeds(myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent, myCurrentSecretInboxSeed, myNextSecretInboxSeed)
|
|
if (err != nil) { return err }
|
|
if (parametersExist == false){
|
|
// This is very unlikely, because we just checked for parameters
|
|
// We cannot determine the secret inbox epoch for the message we have funded
|
|
// We won't be able to download message replies to our sent secret inboxes
|
|
// As long as the user can pass this at least once within the current secret inbox epoch with a message sent
|
|
// to this recipient, they should be able to add their secret inbox seeds to the mySecretInboxes datastore
|
|
// TODO: Use a fallback value so this doesn't happen
|
|
}
|
|
|
|
err = myBroadcasts.BroadcastMyMessage(finalMessage)
|
|
if (err != nil) { return err }
|
|
|
|
err = myChatMessages.AddMyNewMessageToMyChatMessages("Sent", messageHash, messageIdentifier, myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent, messageCommunication)
|
|
if (err != nil) { return err }
|
|
|
|
messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:])
|
|
messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])
|
|
|
|
err = mySentMessagesMapDatastore.SetMapEntry(messageIdentifierHex, messageHashHex)
|
|
if (err != nil){ return err }
|
|
|
|
return nil
|
|
}
|
|
|
|
err := attemptToSendMessage()
|
|
if (err != nil){
|
|
logger.AddLogError("General", err)
|
|
}
|
|
|
|
deleteMessageFromPendingMessagesMap(messageIdentifier)
|
|
pendingMessagesWaitgroup.Done()
|
|
}
|
|
|
|
addMessageToPendingMessagesMap(messageIdentifier)
|
|
pendingMessagesWaitgroup.Add(1)
|
|
|
|
go sendMessageFunction()
|
|
|
|
return false, true, nil
|
|
}
|
|
|
|
// This function calculates current network cost for a provided message
|
|
// This is used to show the user approximately how much a message will cost
|
|
//Outputs:
|
|
// -bool: Parameters exist
|
|
// -float64: Cost in provided currency
|
|
// -error
|
|
func GetMessageNetworkCurrencyCostForProvidedDuration(messageNetworkType byte, messageSizeBytes int, messageDurationToFund int, currencyCode string)(bool, float64, error){
|
|
|
|
isValid := helpers.VerifyNetworkType(messageNetworkType)
|
|
if (isValid == false){
|
|
messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
|
|
return false, 0, errors.New("GetMessageNetworkCurrencyCostForProvidedDuration called with invalid messageNetworkType: " + messageNetworkTypeString)
|
|
}
|
|
|
|
messageKilobytes := messageSizeBytes/1000
|
|
|
|
currentTime := time.Now().Unix()
|
|
|
|
parametersExist, currentCostPerDay, err := getParameters.GetMessageKilobyteGoldCostPerDay(messageNetworkType, currentTime)
|
|
if (err != nil) { return false, 0, err }
|
|
if (parametersExist == false) {
|
|
return false, 0, nil
|
|
}
|
|
|
|
messageDays := messageDurationToFund/86400
|
|
|
|
// GetMessageKilobyteGoldCostPerDay returns grams. We convert to kilograms.
|
|
kilogramsCostPerDay := currentCostPerDay/1000
|
|
|
|
costInKilogramsOfGold := float64(messageDays) * float64(messageKilobytes) * kilogramsCostPerDay
|
|
|
|
exchangeRatesFound, currencyCost, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(messageNetworkType, costInKilogramsOfGold, currencyCode)
|
|
if (err != nil) { return false, 0, err }
|
|
if (exchangeRatesFound == false){
|
|
return false, 0, nil
|
|
}
|
|
|
|
return true, currencyCost, nil
|
|
}
|
|
|
|
// We use this function to estimate the size of a message we have not created yet
|
|
// This enables us to show the user how much the message will cost, and for us to freeze the required funds
|
|
func GetEstimatedMessageSize(recipientIdentityHash [16]byte, messageCommunication string)(int, error){
|
|
|
|
recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash)
|
|
if (err != nil) {
|
|
recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:])
|
|
return 0, errors.New("GetEstimatedMessageSize called with invalid recipientIdentityHash: " + recipientIdentityHashHex)
|
|
}
|
|
|
|
senderIdentityPublicKey, senderIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys()
|
|
if (err != nil) { return 0, err }
|
|
|
|
senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, recipientIdentityType)
|
|
if (err != nil) { return 0, err }
|
|
|
|
senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
|
|
if (err != nil) { return 0, err }
|
|
|
|
senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed()
|
|
if (err != nil) { return 0, err }
|
|
|
|
recipientInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash)
|
|
if (err != nil) { return 0, err }
|
|
|
|
doubleSealedKeysSealerKey, err := helpers.GetNewRandom32ByteArray()
|
|
if (err != nil) { return 0, err }
|
|
|
|
senderLatestChatKeysUpdateTime := time.Now().UnixNano()
|
|
|
|
recipientNaclPublicKey, err := nacl.GetNewRandomPublicNaclKey()
|
|
if (err != nil) { return 0, err }
|
|
|
|
recipientKyberPublicKey, err := kyber.GetNewRandomPublicKyberKey()
|
|
if (err != nil) { return 0, err }
|
|
|
|
messageSentTime := time.Now().Unix()
|
|
|
|
senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier()
|
|
if (err != nil) { return 0, err }
|
|
|
|
finalMessage, _, err := createMessages.CreateChatMessage(1, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclPublicKey, recipientKyberPublicKey, doubleSealedKeysSealerKey)
|
|
if (err != nil) { return 0, err }
|
|
|
|
messageSize := len(finalMessage)
|
|
|
|
return messageSize, nil
|
|
}
|
|
|
|
|
|
|