seekia/internal/messaging/myChatMessages/myChatMessages.go

1359 lines
49 KiB
Go
Raw Permalink Normal View History

// myChatMessages provides functions to manage a user's sent and received chat messages.
// Raw messages are decrypted and then stored in an unencrypted form.
package myChatMessages
//TODO: Prune deleted/undecryptable message hashes lists
// Message hashes should be deleted from this lists once the message expiration time is reached
import "seekia/internal/allowedText"
import "seekia/internal/badgerDatabase"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/identity"
import "seekia/internal/messaging/myChatKeys"
import "seekia/internal/messaging/myCipherKeys"
import "seekia/internal/messaging/myInbox"
import "seekia/internal/messaging/myReadStatus"
import "seekia/internal/messaging/peerChatKeys"
import "seekia/internal/messaging/peerDevices"
import "seekia/internal/messaging/peerSecretInboxes"
import "seekia/internal/messaging/readMessages"
import "seekia/internal/myBlockedUsers"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/myDatastores/myMapList"
import "seekia/internal/myIdentity"
import "seekia/internal/mySettings"
import "seekia/internal/parameters/getParameters"
import "slices"
import "bytes"
import "sync"
import "errors"
import "time"
// This will be locked whenever the chat messages map lists are being updated
var updatingChatMessagesMutex sync.Mutex
var myMateChatMessagesMapListDatastore *myMapList.MyMapList
var myModeratorChatMessagesMapListDatastore *myMapList.MyMapList
// This map stores deleted message hashes
// This map exists to prevent importing/decrypting messages that a user has deleted (or were maliciously crafted).
var myDeletedMessagesMapDatastore *myMap.MyMap
// This map stores unreadable message hashes
// This map exists to prevent attempting to import messages which we cannot decrypt more than once
var myUndecryptableMessagesMapDatastore *myMap.MyMap
func getMyChatMessagesMapListDatastore(myIdentityType string)(*myMapList.MyMapList, error){
if (myIdentityType == "Mate"){
return myMateChatMessagesMapListDatastore, nil
}
if (myIdentityType == "Moderator"){
return myModeratorChatMessagesMapListDatastore, nil
}
return nil, errors.New("getMyChatMessagesMapListDatastore called with invalid myIdentityType: " + myIdentityType)
}
// This function must be called whenever an app user signs in
func InitializeMyChatMessageDatastores()error{
updatingChatMessagesMutex.Lock()
defer updatingChatMessagesMutex.Unlock()
newMyMateChatMessagesMapListDatastore, err := myMapList.CreateNewMapList("MyMateChatMessages")
if (err != nil) { return err }
newMyModeratorChatMessagesMapListDatastore, err := myMapList.CreateNewMapList("MyModeratorChatMessages")
if (err != nil) { return err }
newMyDeletedMessagesMapDatastore, err := myMap.CreateNewMap("MyDeletedMessages")
if (err != nil) { return err }
newMyUndecryptableMessagesMapDatastore, err := myMap.CreateNewMap("MyUndecryptableMessages")
if (err != nil) { return err }
myMateChatMessagesMapListDatastore = newMyMateChatMessagesMapListDatastore
myModeratorChatMessagesMapListDatastore = newMyModeratorChatMessagesMapListDatastore
myDeletedMessagesMapDatastore = newMyDeletedMessagesMapDatastore
myUndecryptableMessagesMapDatastore = newMyUndecryptableMessagesMapDatastore
return nil
}
// This function is called when a user deletes their identity
func DeleteMyChatMessagesMapList(myIdentityType string)error{
if (myIdentityType != "Mate" && myIdentityType != "Moderator"){
return errors.New("DeleteMyChatMessagesMapList called with invalid myIdentityType: " + myIdentityType)
}
updatingChatMessagesMutex.Lock()
defer updatingChatMessagesMutex.Unlock()
myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(myIdentityType)
if (err != nil) { return err }
err = myChatMessagesMapListDatastore.DeleteMapList()
if (err != nil) { return err }
return nil
}
// This function checks to see if chat messages are ready
// Messages become not ready every time a new message is downloaded for one of the user's inboxes
func GetMyChatMessagesReadyStatus(myIdentityType string)(bool, error){
if (myIdentityType != "Mate" && myIdentityType != "Moderator"){
return false, errors.New("GetMyChatMessagesReadyStatus called with invalid myIdentityType: " + myIdentityType)
}
exists, readyStatus, err := mySettings.GetSetting(myIdentityType + "ChatMessagesReadyStatus")
if (err != nil) { return false, err }
if (exists == false || readyStatus != "Yes"){
return false, nil
}
return true, nil
}
// This function returns an updated MyChatMessages map list
// This map list contains decrypted chat messages sent to the user
// The function will attempt to decrypt messages sent to a user's inboxes
// This function will also prune the map list of any different networkType messages
//
// Inputs:
// -string: My Identity Type ("Mate"/"Moderator")
// -byte: Network type (1 == Mainnet, 2 == Testnet 1)
// -func(int)error: Update progress function (will call the function to update between 0-100%)
//Outputs:
// -[]map[string]string: Updated myChatMessages map list
// -error
func GetUpdatedMyChatMessagesMapList(myIdentityType string, networkType byte, updateProgressFunction func(int)error)([]map[string]string, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return nil, errors.New("GetUpdatedMyChatMessagesMapList called with invalid networkType: " + networkTypeString)
}
defer updateProgressFunction(100)
myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(myIdentityType)
if (err != nil) { return nil, err }
messagesReadyStatus, err := GetMyChatMessagesReadyStatus(myIdentityType)
if (err != nil) { return nil, err }
if (messagesReadyStatus == false) {
// We update our chat messages
updateMyChatMessages := func()error{
parametersExist, err := getParameters.CheckIfSecretInboxEpochParametersExist(networkType)
if (err != nil){ return err }
if (parametersExist == false){
// We need these parameters to import user secret inbox information
// We will not update chat messages until we download these parameters
return nil
}
updatingChatMessagesMutex.Lock()
defer updatingChatMessagesMutex.Unlock()
myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType)
if (err != nil) { return err }
if (myIdentityFound == false){
// No chat messages can exist.
err = myChatMessagesMapListDatastore.DeleteMapList()
if (err != nil) { return err }
return nil
}
myInboxesList, err := myInbox.GetAllMyInboxes(myIdentityHash, networkType)
if (err != nil) { return err }
err = updateProgressFunction(5)
if (err != nil) { return err }
// This map will store raw encrypted message hashes for messages in our inboxes
// Map structure: Message Hash -> Message inbox
myRawInboxMessageHashesMap := make(map[[26]byte][10]byte)
for _, inbox := range myInboxesList{
exists, inboxMessageHashesList, err := badgerDatabase.GetChatInboxMessageHashesList(inbox)
if (err != nil) { return err }
if (exists == false) {
continue
}
for _, messageHash := range inboxMessageHashesList{
myRawInboxMessageHashesMap[messageHash] = inbox
}
}
err = updateProgressFunction(10)
if (err != nil) { return err }
if (len(myRawInboxMessageHashesMap) == 0){
// No messages to import, nothing left to do.
return nil
}
myIdentityExists, anyChatKeysExist, myChatDecryptionKeySetsList, err := myChatKeys.GetMyChatDecryptionKeySetsList(myIdentityHash, networkType)
if (err != nil) { return err }
if (myIdentityExists == false) {
return errors.New("My identity not found after already being found.")
}
if (anyChatKeysExist == false){
// The user has never broadcast their chat keys, or has deleted them.
// All messages are undecryptable, unless user imports decryption keys from their last device/profile
return nil
}
err = updateProgressFunction(15)
if (err != nil) { return err }
myExistingMessagesMapList, err := myChatMessagesMapListDatastore.GetMapList()
if (err != nil) { return err }
// This will store the message hashes for messages we already have decrypted and imported
existingMessageHashesMap := make(map[[26]byte]struct{})
for _, messageMap := range myExistingMessagesMapList{
messageIAmSender, exists := messageMap["IAmSender"]
if (exists == false){
return errors.New("Malformed message map: Missing IAmSender")
}
if (messageIAmSender == "Yes"){
continue
}
// Message was not send by user, so message status must be Sent
// Thus, messageHash will exist for all of these messages
currentMessageHashString, exists := messageMap["MessageHash"]
if (exists == false) {
return errors.New("Malformed message map: Missing MessageHash.")
}
currentMessageHash, err := readMessages.ReadMessageHashHex(currentMessageHashString)
if (err != nil){
return errors.New("Malformed message map: contains invalid MessageHash: " + currentMessageHashString)
}
existingMessageHashesMap[currentMessageHash] = struct{}{}
}
err = updateProgressFunction(20)
if (err != nil) { return err }
newMyChatMessagesMapList := make([]map[string]string, 0)
index := 0
maximumIndex := len(myRawInboxMessageHashesMap)
for messageHash, messageInbox := range myRawInboxMessageHashesMap{
newPercentageProgress, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 20, 100)
if (err != nil){ return err }
err = updateProgressFunction(newPercentageProgress)
if (err != nil) { return err }
index += 1
_, messageIsAlreadyAdded := existingMessageHashesMap[messageHash]
if (messageIsAlreadyAdded == true) {
continue
}
messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])
hasBeenDeleted, _, err := myDeletedMessagesMapDatastore.GetMapEntry(messageHashHex)
if (err != nil) { return err }
if (hasBeenDeleted == true) {
continue
}
isUndecryptable, _, err := myUndecryptableMessagesMapDatastore.GetMapEntry(messageHashHex)
if (err != nil) { return err }
if (isUndecryptable == true) {
// These are messages which we have already tried to import but were not able to
// This could be due to missing chat keys or a malicious sender
// If user ever imports new chat keys from a different device,
// the undecryptable messages map is deleted, so we can try again for all messages that haven't been deleted
continue
}
inboxFound, inboxMyIdentityHash, inboxDoubleSealedKeysSealerKey, inboxIsSecret, inboxNetworkType, conversationRecipient, err := myInbox.GetMyInboxInfo(messageInbox)
if (err != nil) { return err }
if (inboxFound == false){
// We know that the inbox is not a public inbox.
// We do not have the sealer key for this inbox. We cannot decrypt the message.
currentTime := time.Now().Unix()
currentTimeString := helpers.ConvertInt64ToString(currentTime)
err := myUndecryptableMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString)
if (err != nil) { return err }
continue
}
if (inboxMyIdentityHash != myIdentityHash){
return errors.New("GetAllMyInboxes is returning inbox for a different identity hash.")
}
if (inboxIsSecret == true && inboxNetworkType != networkType){
return errors.New("GetAllMyInboxes is returning a secret inbox which belongs to a different network type.")
}
// Now we attempt to decrypt the message
messageExists, messageBytes, err := badgerDatabase.GetChatMessage(messageHash)
if (err != nil) { return err }
if (messageExists == false){
// Database inbox messages list is outdated. It will be updated automatically.
continue
}
ableToRead, _, messageNetworkType, messageInbox_Received, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicData(false, messageBytes)
if (err != nil) { return err }
if (ableToRead == false){
return errors.New("Database corrupt: Contains message with unreadable public data.")
}
if (messageInbox != messageInbox_Received){
return errors.New("myRawInboxMessageHashesMap contains mismatched message inbox value.")
}
if (messageNetworkType != networkType){
// This message was sent to us on a different network type
// If the inbox is public, this is fine and expected
// If the inbox is secret, then the sender must be malicious (or our client is acting improperly)
if (inboxIsSecret == true){
currentTime := time.Now().Unix()
currentTimeString := helpers.ConvertInt64ToString(currentTime)
err := myDeletedMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString)
if (err != nil) { return err }
//TODO: Log this
}
continue
}
ableToRead, _, _, _, messageCipherKey, senderIdentityHash, recipientIdentityHash, messageCreationTimeUnix, messageCommunication, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, err := readMessages.ReadChatMessage(messageBytes, inboxDoubleSealedKeysSealerKey, myChatDecryptionKeySetsList)
if (err != nil) { return err }
if (ableToRead == false) {
// Either keys are lost/deleted or sender is forming malicious messages
// Skip this message.
//TODO: ReadChatMessage should return 3 bools: outer is malformed, Undecryptable, inner is malformed
// If the inner message is malformed, sender is malicious and we should add the message to the deletedMessagesMapDatastore
currentTime := time.Now().Unix()
currentTimeString := helpers.ConvertInt64ToString(currentTime)
err := myUndecryptableMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString)
if (err != nil) { return err }
continue
}
if (recipientIdentityHash != myIdentityHash){
// Message is sent to my inbox, but not my identity
// Sender is either malicious, or sender is being tricked by another user who is using my chat keys and sending secret inboxes that are my inboxes.
// Either way, ignore message and delete it, never try to import it again.
currentTime := time.Now().Unix()
currentTimeString := helpers.ConvertInt64ToString(currentTime)
err := myDeletedMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString)
if (err != nil) { return err }
continue
}
senderIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(senderIdentityHash)
if (err != nil) { return err }
if (senderIsBlocked == true){
// We will not add messages sent from users we have blocked
continue
}
if (inboxIsSecret == true){
// Secret inboxes are reserved for a specific recipient
if (senderIdentityHash != conversationRecipient){
// Sender is either malicious, or sender is being tricked by
// another user who is using my chat keys and sending secret inboxes that are my inboxes.
// Either way, ignore message and delete it, never import it again
currentTime := time.Now().Unix()
currentTimeString := helpers.ConvertInt64ToString(currentTime)
err := myDeletedMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString)
if (err != nil) { return err }
continue
}
}
err = peerChatKeys.SavePeerMessageLatestChatKeysUpdateTime(senderIdentityHash, networkType, senderLatestChatKeysUpdateTime, messageCreationTimeUnix)
if (err != nil) { return err }
parametersExist, err = peerSecretInboxes.AddPeerConversationSecretInboxSeeds(myIdentityHash, senderIdentityHash, messageNetworkType, messageCreationTimeUnix, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed)
if (err != nil) { return err }
if (parametersExist == false){
// This means we cannot add the sender's secret inboxes to our storage
// This means we will send all messages to their public inbox, reducing privacy
// This should not happen, because we already checked for parameters before starting this
// We will skip all the rest of the messages and try again when we have the parameters
break
}
err = myCipherKeys.SaveMessageCipherKey(messageHash, messageCipherKey)
if (err != nil) { return err }
err = peerDevices.AddPeerDeviceIdentifierFromMessage(senderIdentityHash, messageNetworkType, senderDeviceIdentifier, messageCreationTimeUnix)
if (err != nil) { return err }
//TODO: Verify communication. For example, verify that image is valid, greet/reject is valid, etc.
err = myReadStatus.SetConversationReadUnreadStatus(myIdentityHash, senderIdentityHash, networkType, "Unread")
if (err != nil) { return err }
// We add message to the MyChatMessages map list
messageContentMap, err := getNewMessageMap("Sent", messageHash, [20]byte{}, myIdentityHash, senderIdentityHash, messageNetworkType, false, messageCreationTimeUnix, messageCommunication)
if (err != nil) { return err }
newMyChatMessagesMapList = append(newMyChatMessagesMapList, messageContentMap)
}
if (len(newMyChatMessagesMapList) != 0){
// Some messages were successfully decrypted.
// We add them to our chat messages map list.
allMyChatMessagesMapList := slices.Concat(myExistingMessagesMapList, newMyChatMessagesMapList)
err = myChatMessagesMapListDatastore.OverwriteMapList(allMyChatMessagesMapList)
if (err != nil) { return err }
}
return nil
}
err := updateMyChatMessages()
if (err != nil) { return nil, err }
err = mySettings.SetSetting(myIdentityType + "ChatMessagesReadyStatus", "Yes")
if (err != nil) { return nil, err }
}
networkTypeString := helpers.ConvertByteToString(networkType)
lookupMap := map[string]string{
"NetworkType": networkTypeString,
}
anyExist, updatedMapList, err := myChatMessagesMapListDatastore.GetMapListItems(lookupMap)
if (err != nil) { return nil, err }
if (anyExist == false){
emptyMapList := make([]map[string]string, 0)
return emptyMapList, nil
}
return updatedMapList, nil
}
func CheckIfUserHasMessagedMe(theirIdentityHash [16]byte, networkType byte)(bool, error){
theirIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash)
if (err != nil) {
theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:])
return false, errors.New("CheckIfUserHasMessagedMe called with invalid theirIdentityHash: " + theirIdentityHashHex)
}
if (userIdentityType == "Host"){
return false, errors.New("CheckIfUserHasMessagedMe called with host identity.")
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, errors.New("CheckIfUserHasMessagedMe called with invalid networkType: " + networkTypeString)
}
updateProgressFunction := func(_ int)error{
return nil
}
myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(userIdentityType, networkType, updateProgressFunction)
if (err != nil) { return false, err }
for _, messageMap := range myChatMessagesMapList{
messageTheirIdentityHash, exists := messageMap["TheirIdentityHash"]
if (exists == false) {
return false, errors.New("Malformed chat messages map list: missing theirIdentityHash")
}
if (messageTheirIdentityHash != theirIdentityHashString){
continue
}
iAmSender, exists := messageMap["IAmSender"]
if (exists == false) {
return false, errors.New("Malformed myChatMessagesMapList: Missing IAmSender")
}
if (iAmSender == "No"){
return true, nil
}
}
return false, nil
}
func CheckIfIHaveMessagedUser(theirIdentityHash [16]byte, networkType byte)(bool, error){
theirIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash)
if (err != nil) {
theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:])
return false, errors.New("CheckIfIHaveMessagedUser called with invalid theirIdentityHash: " + theirIdentityHashHex)
}
if (userIdentityType == "Host"){
return false, errors.New("CheckIfIHaveMessagedUser called with host identity")
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, errors.New("CheckIfIHaveMessagedUser called with invalid networkType: " + networkTypeString)
}
updateProgressFunction := func(_ int)error{
return nil
}
myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(userIdentityType, networkType, updateProgressFunction)
if (err != nil) { return false, err }
for _, messageMap := range myChatMessagesMapList{
messageTheirIdentityHash, exists := messageMap["TheirIdentityHash"]
if (exists == false) {
return false, errors.New("Malformed chat messages map list: missing TheirIdentityHash")
}
if (messageTheirIdentityHash != theirIdentityHashString){
continue
}
iAmSender, exists := messageMap["IAmSender"]
if (exists == false) {
return false, errors.New("Malformed myChatMessagesMapList: Missing IAmSender")
}
if (iAmSender == "Yes"){
return true, nil
}
}
return false, nil
}
// This function retrieves the number of messages that exist between the user and another user
// We use this to warn the user before blocking a user about how many messages will be deleted
func GetNumberOfMyConversationMessages(theirIdentityHash [16]byte, networkType byte)(int, error){
theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash)
if (err != nil) {
theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:])
return 0, errors.New("GetNumberOfMyConversationMessages called with invalid theirIdentityHash: " + theirIdentityHashHex)
}
if (theirIdentityType == "Host"){
return 0, errors.New("GetNumberOfMyConversationMessages called with host identity.")
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return 0, errors.New("GetNumberOfMyConversationMessages called with invalid networkType: " + networkTypeString)
}
updateProgressFunction := func(_ int)error{
return nil
}
chatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(theirIdentityType, networkType, updateProgressFunction)
if (err != nil) { return 0, err }
numberOfMessages := 0
for _, messageMap := range chatMessagesMapList{
currentTheirIdentityHash, exists := messageMap["TheirIdentityHash"]
if (exists == false) {
return 0, errors.New("Malformed chat messages map list: missing TheirIdentityHash")
}
if (currentTheirIdentityHash == theirIdentityHashString){
numberOfMessages += 1
}
}
return numberOfMessages, nil
}
func GetNumberOfMessagesInMyInbox(myIdentityType string, networkType byte)(int, error){
identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType)
if (err != nil) { return 0, err }
if (identityExists == false) {
return 0, nil
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return 0, errors.New("GetNumberOfMessagesInMyInbox called with invalid networkType: " + networkTypeString)
}
updateProgressFunction := func(_ int)error{
return nil
}
myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction)
if (err != nil) { return 0, err }
numberOfInboxMessages := 0
for _, messageMap := range myChatMessagesMapList{
messageMyIdentityHashString, exists := messageMap["MyIdentityHash"]
if (exists == false) {
return 0, errors.New("Malformed my chat messages map list: Item missing MyIdentityHash")
}
messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString)
if (err != nil){
return 0, errors.New("Malformed myChatMessages map list: Item contains invalid MyIdentityHash: " + messageMyIdentityHashString)
}
if (messageMyIdentityHash != myIdentityHash){
return 0, errors.New("Malformed myChatMessages map list: Contains message that does not belong to my identity.")
}
iAmSender, exists := messageMap["IAmSender"]
if (exists == false) {
return 0, errors.New("Malformed myChatMessagesMapList: Item missing IAmSender")
}
if (iAmSender == "No"){
numberOfInboxMessages += 1
}
}
return numberOfInboxMessages, nil
}
func CheckIfMessageIsDeleted(messageHash [26]byte)(bool, error){
messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])
hasBeenDeleted, _, err := myDeletedMessagesMapDatastore.GetMapEntry(messageHashHex)
if (err != nil) { return false, err }
return hasBeenDeleted, nil
}
func CheckIfMessageIsUndecryptable(messageHash [26]byte)(bool, error){
messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])
isUndecryptable, _, err := myUndecryptableMessagesMapDatastore.GetMapEntry(messageHashHex)
if (err != nil) { return false, err }
return isUndecryptable, nil
}
func CheckIfMessageIsImported(messageHash [26]byte, identityType string, messageNetworkType byte)(bool, error){
if (identityType != "Mate" && identityType != "Moderator"){
return false, errors.New("CheckIfMessageIsImported called with invalid identityType: " + identityType)
}
isValid := helpers.VerifyNetworkType(messageNetworkType)
if (isValid == false){
messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
return false, errors.New("CheckIfMessageIsImported called with invalid messageNetworkType: " + messageNetworkTypeString)
}
messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])
updateProgressFunction := func(_ int)error{
return nil
}
myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(identityType, messageNetworkType, updateProgressFunction)
if (err != nil) { return false, err }
for _, messageMap := range myChatMessagesMapList{
currentMessageHash, exists := messageMap["MessageHash"]
if (exists == true){
if (messageHashHex == currentMessageHash){
return true, nil
}
}
}
return false, nil
}
// We use this type to represent messages returned from the GetMyConversationInfoAndSortedMessagesList function
type ConversationMessage struct{
// "Queued"/"Sent"/"Failed"
MessageStatus string
// Sent == Message has been constructed and is in broadcast queue/has been broadcast
// If message is sent, then MessageHash exists.
MessageIsSent bool
// If MessageHash exists, then MessageIdentifier is empty
// If MessageIdentifier exists, then MessageHash is empty
MessageHash [26]byte
MessageIdentifier [20]byte
// Unix time of message creation time
CreationTime int64
// True if I am message sender, false if not.
IAmSender bool
// The contents of the message communication
Communication string
}
//This function returns all messages for a conversation and metadata about the conversation
//Inputs:
// -[16]byte: My Identity Hash
// -[16]byte: Their Identity Hash
// -byte: Network type of conversation
//Outputs:
// -bool: My identity found
// -bool: Conversation found (any messages found = true, no messages found = false)
// -[]ConversationMessage: Messages list (sorted by creation time)
// -int64: Conversation latest message creation time
// -bool: I have contacted them
// -bool: I have rejected them (if myIdentityType == Mate)
// -int64: Time of my most recent greet/rejection to them
// -bool: They have contacted me
// -bool: They have rejected me (if myIdentityType == Mate)
// -int64: Time of their most recent greet/rejection to me
// -error
func GetMyConversationInfoAndSortedMessagesList(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte) (bool, bool, []ConversationMessage, int64, bool, bool, int64, bool, bool, int64, error){
myIdentityFound, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash)
if (err != nil) { return false, false, nil, 0, false, false, 0, false, false, 0, err }
if (myIdentityFound == false){
return false, false, nil, 0, false, false, 0, false, false, 0, nil
}
theirIdentityType, err := identity.GetIdentityTypeFromIdentityHash(theirIdentityHash)
if (err != nil) {
theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:])
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("GetMyConversationInfoAndSortedMessagesList called with invalid theirIdentityHash: " + theirIdentityHashHex)
}
if (theirIdentityType != myIdentityType){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("GetMyConversationInfoAndSortedMessagesList called with mismatched identityType identities.")
}
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("GetMyConversationInfoAndSortedMessagesList called with invalid networkType: " + networkTypeString)
}
updateProgressFunction := func(_ int)error{
return nil
}
myChatMessagesList, err := GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction)
if (err != nil) { return false, false, nil, 0, false, false, 0, false, false, 0, err }
if (len(myChatMessagesList) == 0) {
return true, false, nil, 0, false, false, 0, false, false, 0, nil
}
conversationMessagesList := make([]ConversationMessage, 0)
lastMessageCreationTimeUnix := int64(0)
for _, messageMap := range myChatMessagesList {
messageMyIdentityHashString, exists := messageMap["MyIdentityHash"]
if (exists == false) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MyIdentityHash")
}
messageTheirIdentityHashString, exists := messageMap["TheirIdentityHash"]
if (exists == false) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing TheirIdentityHash")
}
messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString)
if (err != nil){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MyIdentityHash: " + messageMyIdentityHashString)
}
messageTheirIdentityHash, _, err := identity.ReadIdentityHashString(messageTheirIdentityHashString)
if (err != nil){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid TheirIdentityHash: " + messageTheirIdentityHashString)
}
if (messageMyIdentityHash != myIdentityHash){
// This must be a message from an earlier identity which we deleted
// This should not happen, because all of our identity's messages should be deleted when we delete our identity
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains message for identity which is not our current identity.")
}
if (messageTheirIdentityHash != theirIdentityHash) {
continue
}
messageStatus, exists := messageMap["MessageStatus"]
if (exists == false){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MessageStatus.")
}
iAmSenderString, exists := messageMap["IAmSender"]
if (exists == false) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing IAmSender.")
}
creationTimeUnixString, exists := messageMap["CreationTime"]
if (exists == false) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing CreationTime.")
}
communicationString, exists := messageMap["Communication"]
if (exists == false) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing Communication")
}
iAmSender, err := helpers.ConvertYesOrNoStringToBool(iAmSenderString)
if (err != nil){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid IAmSender: " + iAmSenderString)
}
creationTimeUnix, err := helpers.ConvertStringToInt64(creationTimeUnixString)
if (err != nil) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid CreationTime: " + creationTimeUnixString)
}
if (lastMessageCreationTimeUnix < creationTimeUnix){
lastMessageCreationTimeUnix = creationTimeUnix
}
newMessageObject := ConversationMessage{}
newMessageObject.MessageStatus = messageStatus
if (messageStatus == "Sent"){
newMessageObject.MessageIsSent = true
messageHashString, exists := messageMap["MessageHash"]
if (exists == false) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MessageHash.")
}
messageHashArray, err := readMessages.ReadMessageHashHex(messageHashString)
if (err != nil) {
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MessageHash: " + messageHashString)
}
newMessageObject.MessageHash = messageHashArray
} else {
newMessageObject.MessageIsSent = false
messageIdentifierString, exists := messageMap["MessageIdentifier"]
if (exists == false){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MessageIdentifier.")
}
messageIdentifierBytes, err := encoding.DecodeHexStringToBytes(messageIdentifierString)
if (err != nil){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MessageIdentifier: Not Hex: " + messageIdentifierString)
}
if (len(messageIdentifierBytes) != 20){
return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MessageIdentifier: Invalid length: " + messageIdentifierString)
}
messageIdentifierArray := [20]byte(messageIdentifierBytes)
newMessageObject.MessageIdentifier = messageIdentifierArray
}
newMessageObject.CreationTime = creationTimeUnix
newMessageObject.IAmSender = iAmSender
newMessageObject.Communication = communicationString
conversationMessagesList = append(conversationMessagesList, newMessageObject)
}
if (len(conversationMessagesList) == 0){
return true, false, nil, 0, false, false, 0, false, false, 0, nil
}
// Now we sort messages oldest to newest
compareMessagesFunction := func(message1 ConversationMessage, message2 ConversationMessage)int {
message1CreationTime := message1.CreationTime
message2CreationTime := message2.CreationTime
if (message1CreationTime == message2CreationTime){
// We sort messages so they always appear in the same order
message1IAmSender := message1.IAmSender
message2IAmSender := message2.IAmSender
if (message1IAmSender == message2IAmSender){
message1Communication := message1.Communication
message2Communication := message2.Communication
if (message1Communication == message2Communication){
return 0
}
if (message1Communication < message2Communication){
return -1
}
return 1
}
if (message1IAmSender == false){
return -1
}
return 1
}
if (message1CreationTime < message2CreationTime){
return -1
}
return 1
}
slices.SortFunc(conversationMessagesList, compareMessagesFunction)
// Now we get conversation greet or reject status
// A user has greeted another user if they have sent them any message. It does not have to be a Greet message
// A rejection can only be undone by a greet
// Any messages sent after a rejection do not cancel the rejection, unless they are Greet messages
// Moderators cannot send greet/reject messages. TODO: Deal with this reality.
iHaveContactedThem := false
iHaveRejectedThem := false
timeOfMyMostRecentGreetOrReject := int64(0)
theyHaveContactedMe := false
theyHaveRejectedMe := false
timeOfTheirMostRecentGreetOrReject := int64(0)
// We iterate through list from oldest to newest message
for _, currentMessageObject := range conversationMessagesList{
iAmSender := currentMessageObject.IAmSender
if (iAmSender == true){
iHaveContactedThem = true
} else {
theyHaveContactedMe = true
}
messageCreationTime := currentMessageObject.CreationTime
messageCommunication := currentMessageObject.Communication
if (messageCommunication == ">!>Greet"){
if (iAmSender == true){
iHaveRejectedThem = false
timeOfMyMostRecentGreetOrReject = messageCreationTime
} else {
theyHaveRejectedMe = false
timeOfTheirMostRecentGreetOrReject = messageCreationTime
}
} else if (messageCommunication == ">!>Reject"){
if (iAmSender == true){
iHaveRejectedThem = true
timeOfMyMostRecentGreetOrReject = messageCreationTime
} else {
theyHaveRejectedMe = true
timeOfTheirMostRecentGreetOrReject = messageCreationTime
}
} else {
if (iAmSender == true){
if (iHaveRejectedThem == false){
timeOfMyMostRecentGreetOrReject = messageCreationTime
}
} else{
if (theyHaveRejectedMe == false){
timeOfTheirMostRecentGreetOrReject = messageCreationTime
}
}
}
}
return true, true, conversationMessagesList, lastMessageCreationTimeUnix, iHaveContactedThem, iHaveRejectedThem, timeOfMyMostRecentGreetOrReject, theyHaveContactedMe, theyHaveRejectedMe, timeOfTheirMostRecentGreetOrReject, nil
}
func DeleteAllPeerConversationMessages(theirIdentityHash [16]byte)error{
theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash)
if (err != nil){
theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:])
return errors.New("DeleteAllPeerConversationMessages called with invalid theirIdentityHash: " + theirIdentityHashHex)
}
if (theirIdentityType == "Host"){
return errors.New("DeleteAllPeerConversationMessages called with host identity.")
}
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(theirIdentityType)
if (err != nil) { return err }
if (myIdentityExists == false){
// No messages should exist from this user.
return nil
}
updatingChatMessagesMutex.Lock()
defer updatingChatMessagesMutex.Unlock()
myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(theirIdentityType)
if (err != nil) { return err }
myChatMessagesMapList, err := myChatMessagesMapListDatastore.GetMapList()
if (err != nil) { return err }
newMyChatMessagesMapList := make([]map[string]string, 0)
for _, messageMap := range myChatMessagesMapList{
messageMyIdentityHashString, exists := messageMap["MyIdentityHash"]
if (exists == false) {
return errors.New("Malformed chat messages map list: missing MyIdentityHash")
}
messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString)
if (err != nil){
return errors.New("Malformed chat messages map list: Contains invalid MyIdentityHash: " + messageMyIdentityHashString)
}
if (messageMyIdentityHash != myIdentityHash){
// A message exists that does not belong to our identity
// This should not happen, because when we delete our identity, we delete all of our chat messages
return errors.New("MyChatMessagesMapList contains message with identity that is not ours.")
}
messageTheirIdentityHash, exists := messageMap["TheirIdentityHash"]
if (exists == false) {
return errors.New("Malformed myChatMessagesMapList: Item missing TheirIdentityHash")
}
if (messageTheirIdentityHash != theirIdentityHashString){
newMyChatMessagesMapList = append(newMyChatMessagesMapList, messageMap)
}
}
err = myChatMessagesMapListDatastore.OverwriteMapList(newMyChatMessagesMapList)
if (err != nil) { return err }
return nil
}
//Outputs:
// -bool: My identity exists
// -[][16]byte: List of all chat recipients that the user has not blocked
// -error
func GetAllMyNonBlockedChatRecipients(myIdentityType string, networkType byte)(bool, [][16]byte, error){
if (myIdentityType != "Mate" && myIdentityType != "Moderator"){
return false, nil, errors.New("GetAllMyNonBlockedChatRecipients called with invalid identity type: " + myIdentityType)
}
identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType)
if (err != nil) { return false, nil, err }
if (identityExists == false){
return false, nil, nil
}
updateProgressFunction := func(_ int)error{
return nil
}
myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction)
if (err != nil) { return false, nil, err }
if (len(myChatMessagesMapList) == 0){
emptyList := make([][16]byte, 0)
return true, emptyList, nil
}
// We use a map to avoid duplicates
// Map Structure: Recipient Identity Hash -> Nothing
chatRecipientsMap := make(map[[16]byte]struct{})
for _, messageMap := range myChatMessagesMapList{
messageMyIdentityHashString, exists := messageMap["MyIdentityHash"]
if (exists == false) {
return false, nil, errors.New("Malformed myChatMessagesMapList: Item missing MyIdentityHash")
}
messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString)
if (err != nil){
return false, nil, errors.New("Malformed myChatMessagesMapList: Item contains invalid MyIdentityHash: " + messageMyIdentityHashString)
}
if (messageMyIdentityHash != myIdentityHash){
return false, nil, errors.New("myChatMessagesMapList contains message that does not belong to my identity.")
}
messageTheirIdentityHashString, exists := messageMap["TheirIdentityHash"]
if (exists == false) {
return false, nil, errors.New("Malformed myChatMessagesMapList: Item missing TheirIdentityHash")
}
messageTheirIdentityHash, _, err := identity.ReadIdentityHashString(messageTheirIdentityHashString)
if (err != nil){
return false, nil, errors.New("Malformed myChatMessagesMapList: Item contains invalid TheirIdentityHash: " + messageTheirIdentityHashString)
}
chatRecipientsMap[messageTheirIdentityHash] = struct{}{}
}
nonBlockedChatRecipientsList := make([][16]byte, 0)
for userIdentityHash, _ := range chatRecipientsMap{
isBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(userIdentityHash)
if (err != nil) { return false, nil, err }
if (isBlocked == false){
nonBlockedChatRecipientsList = append(nonBlockedChatRecipientsList, userIdentityHash)
}
}
return true, nonBlockedChatRecipientsList, nil
}
// This function adds a new user sent/failed/queued message to myChatMessages map list where user is sender
// We also remove the queued message map if message is sent/failed
func AddMyNewMessageToMyChatMessages(
messageStatus string,
messageHash [26]byte,
messageIdentifier [20]byte,
myIdentityHash [16]byte,
theirIdentityHash [16]byte,
messageNetworkType byte,
messageCreationTime int64,
communication string)error{
identityIsMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash)
if (err != nil) { return err }
if (identityIsMine == false){
return errors.New("AddMyNewMessageToMyChatMessages called with identity that is not mine.")
}
if (myIdentityType != "Mate" && myIdentityType != "Moderator"){
return errors.New("AddMyNewMessageToMyChatMessages called with host identity.")
}
newMessageMap, err := getNewMessageMap(messageStatus, messageHash, messageIdentifier, myIdentityHash, theirIdentityHash, messageNetworkType, true, messageCreationTime, communication)
if (err != nil) { return err }
updatingChatMessagesMutex.Lock()
defer updatingChatMessagesMutex.Unlock()
myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(myIdentityType)
if (err != nil) { return err }
existingMessagesMapList, err := myChatMessagesMapListDatastore.GetMapList()
if (err != nil) { return err }
getNewChatMessagesMapList := func()([]map[string]string, error){
if (messageStatus == "Queued"){
newChatMessagesMapList := append(existingMessagesMapList, newMessageMap)
return newChatMessagesMapList, nil
}
// Chat message status is either Sent or Failed
// This means it will replace an existing queued message
// We remove that message from the existing map list
newChatMessagesMapList := make([]map[string]string, 0)
for _, messageMap := range existingMessagesMapList{
currentMessageIdentifierString, exists := messageMap["MessageIdentifier"]
if (exists == false){
return nil, errors.New("MyChatMessages map list item missing MessageIdentifier")
}
currentMessageIdentifier, err := encoding.DecodeHexStringToBytes(currentMessageIdentifierString)
if (err != nil){
return nil, errors.New("MyChatMessages map list item contains invalid MessageIdentifier: " + currentMessageIdentifierString)
}
if (len(currentMessageIdentifier) != 20){
return nil, errors.New("MyChatMessages map list item contains invalid MessageIdentifier: " + currentMessageIdentifierString)
}
areEqual := bytes.Equal(currentMessageIdentifier, messageIdentifier[:])
if (areEqual == false){
newChatMessagesMapList = append(newChatMessagesMapList, messageMap)
continue
}
currentMessageStatus, exists := messageMap["MessageStatus"]
if (exists == false){
return nil, errors.New("MyChatMessages map list item missing MessageStatus")
}
if (currentMessageStatus != "Queued"){
return nil, errors.New("AddMyNewMessageToMyChatMessages called to replace existing message that is not queued: " + currentMessageStatus)
}
// This is reached if the message is the one which we want to remove
// We do not add the message
}
newChatMessagesMapList = append(newChatMessagesMapList, newMessageMap)
return newChatMessagesMapList, nil
}
newChatMessagesMapList, err := getNewChatMessagesMapList()
if (err != nil) { return err }
err = myChatMessagesMapListDatastore.OverwriteMapList(newChatMessagesMapList)
if (err != nil) { return err }
return nil
}
// MessageHash should be empty ("") if messageStatus == "Queued" or "Failed"
// messageIdentifier should be empty if messageStatus == "Sent"
func getNewMessageMap(messageStatus string, messageHash [26]byte, messageIdentifier [20]byte, myIdentityHash [16]byte, theirIdentityHash [16]byte, messageNetworkType byte, iAmSender bool, messageCreationTime int64, communication string)(map[string]string, error){
if (messageStatus != "Sent" && messageStatus != "Queued" && messageStatus != "Failed"){
return nil, errors.New("getNewMessageMap called with invalid messageStatus: " + messageStatus)
}
myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash)
if (err != nil) {
myIdentityHashString := encoding.EncodeBytesToHexString(myIdentityHash[:])
return nil, errors.New("getNewMessageMap called with invalid my identity hash: " + myIdentityHashString)
}
theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash)
if (err != nil) {
theirIdentityHashString := encoding.EncodeBytesToHexString(theirIdentityHash[:])
return nil, errors.New("getNewMessageMap called with invalid theirIdentityHash: " + theirIdentityHashString)
}
if (myIdentityType != theirIdentityType){
return nil, errors.New("Trying to create message content map between different identityTypes")
}
if (myIdentityType == "Host"){
return nil, errors.New("Trying to create message content map from Host identityType")
}
isValid := helpers.VerifyNetworkType(messageNetworkType)
if (isValid == false){
messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
return nil, errors.New("getNewMessageMap called with invalid messageNetworkType: " + messageNetworkTypeString)
}
messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType)
iAmSenderString := helpers.ConvertBoolToYesOrNoString(iAmSender)
if (messageCreationTime > time.Now().Unix() + 10){
return nil, errors.New("getNewMessageMap called with invalid creationTime: is in the future.")
}
isAllowed := allowedText.VerifyStringIsAllowed(communication)
if (isAllowed == false){
return nil, errors.New("getNewMessageMap called with communication containing unallowed text.")
}
messageCreationTimeString := helpers.ConvertInt64ToString(messageCreationTime)
messageContentMap := map[string]string{
"MessageStatus": messageStatus,
"MyIdentityHash": myIdentityHashString,
"TheirIdentityHash": theirIdentityHashString,
"NetworkType": messageNetworkTypeString,
"IAmSender": iAmSenderString,
"CreationTime": messageCreationTimeString,
"Communication": communication,
}
if (messageStatus == "Sent"){
messageHashHex := encoding.EncodeBytesToHexString(messageHash[:])
messageContentMap["MessageHash"] = messageHashHex
} else {
// messageStatus == "Queued" || messageStatus == "Failed"
// Queued messages are messages that are queued to be sent, but are not sent yet
// They are displayed differently in the GUI
// They do not have a messageHash, because the raw encrypted message has not been created yet
// Failed messages are messages for which we were not able to broadcast, so they don't have a message hash either.
messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:])
messageContentMap["MessageIdentifier"] = messageIdentifierHex
}
return messageContentMap, nil
}