// myBroadcasts provides functions to manage a user's broadcast content // This is content that will be broadcast or has already been broadcast // The app will automatically broadcast the content in the background package myBroadcasts //TODO: Add reports and parameters //TODO: Keep track of how many times each piece of content has been broadcast, and use that information to // broadcast older content less over time //TODO: Add functions to prune old broadcasted profiles, messages, reports, and parameters import "seekia/internal/cryptography/nacl" import "seekia/internal/cryptography/kyber" import "seekia/internal/encoding" import "seekia/internal/helpers" import "seekia/internal/identity" import "seekia/internal/localFilesystem" import "seekia/internal/messaging/myChatKeys" import "seekia/internal/messaging/readMessages" import "seekia/internal/moderation/mySkippedContent" import "seekia/internal/moderation/readReviews" import "seekia/internal/moderation/reviewStorage" import "seekia/internal/myIdentity" import "seekia/internal/network/appNetworkType/getAppNetworkType" import "seekia/internal/profiles/calculatedAttributes" import "seekia/internal/profiles/myProfileExports" import "seekia/internal/profiles/profileStorage" import "seekia/internal/profiles/readProfiles" import messagepack "github.com/vmihailenco/msgpack/v5" import goFilepath "path/filepath" import "os" import "errors" func InitializeMyBroadcastsFolders()error{ userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } myBroadcastsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts") _, err = localFilesystem.CreateFolder(myBroadcastsFolderpath) if (err != nil) { return err } profilesFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles") messagesFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Messages") reportsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reports") parametersFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Parameters") folderpathsList := []string{profilesFolderpath, messagesFolderpath, reportsFolderpath, parametersFolderpath} for _, folderpath := range folderpathsList{ _, err := localFilesystem.CreateFolder(folderpath) if (err != nil) { return err } network1Folderpath := goFilepath.Join(folderpath, "Network1") network2Folderpath := goFilepath.Join(folderpath, "Network2") _, err = localFilesystem.CreateFolder(network1Folderpath) if (err != nil) { return err } _, err = localFilesystem.CreateFolder(network2Folderpath) if (err != nil) { return err } } reviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews") _, err = localFilesystem.CreateFolder(reviewsFolderpath) if (err != nil) { return err } // We create reviews subfolders for each reviewType // We use subfolders so retrieval is faster // The speedup will be significant for moderators with tens of thousands of reviews // Reports do not need subfolders because users will typically create very few reports identityReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Identity") profileReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Profile") attributeReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Attribute") messageReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Message") folderpathsList = []string{identityReviewsFolderpath, profileReviewsFolderpath, attributeReviewsFolderpath, messageReviewsFolderpath} for _, folderpath := range folderpathsList{ _, err := localFilesystem.CreateFolder(folderpath) if (err != nil) { return err } network1Folderpath := goFilepath.Join(folderpath, "Network1") network2Folderpath := goFilepath.Join(folderpath, "Network2") _, err = localFilesystem.CreateFolder(network1Folderpath) if (err != nil) { return err } _, err = localFilesystem.CreateFolder(network2Folderpath) if (err != nil) { return err } } return nil } //Outputs: // -bool: My Identity exists // -bool: Profile exists // -int: Profile version // -bool: Attribute exists // -string: Attribute value // -error func GetAnyAttributeFromMyBroadcastProfile(myIdentityHash [16]byte, networkType byte, attribute string)(bool, bool, int, bool, string, error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, false, 0, false, "", errors.New("GetAnyAttributeFromMyBroadcastProfile called with invalid networkType: " + networkTypeString) } identityExists, profileExists, _, getAnyAttributeFunction, err := GetRetrieveAnyAttributeFromMyBroadcastProfileFunction(myIdentityHash, networkType) if (err != nil) { return false, false, 0, false, "", err } if (identityExists == false){ return false, false, 0, false, "", nil } if (profileExists == false){ return true, false, 0, false, "", nil } attributeExists, profileVersion, attributeValue, err := getAnyAttributeFunction(attribute) if (err != nil) { return false, false, 0, false, "", err } if (attributeExists == false){ return true, true, 0, false, "", nil } return true, true, profileVersion, true, attributeValue, nil } //Outputs: // -bool: My identity exists // -bool: Profile exists // -[28]byte: Profile hash // -func(attributeName string)(bool, int, string, error) // -error func GetRetrieveAnyAttributeFromMyBroadcastProfileFunction(myIdentityHash [16]byte, networkType byte)(bool, bool, [28]byte, func(string)(bool, int, string, error), error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, false, [28]byte{}, nil, errors.New("GetRetrieveAnyAttributeFromMyBroadcastProfileFunction called with invalid networkType: " + networkTypeString) } identityExists, _, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) if (err != nil) { return false, false, [28]byte{}, nil, err } if (identityExists == false){ return false, false, [28]byte{}, nil, nil } profileExists, profileVersion, profileHash, _, rawProfileMap, err := GetMyNewestBroadcastProfile(myIdentityHash, networkType) if (err != nil) { return false, false, [28]byte{}, nil, err } if (profileExists == false){ return true, false, [28]byte{}, nil, nil } getAnyAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, rawProfileMap) if (err != nil) { return false, false, [28]byte{}, nil, err } return true, true, profileHash, getAnyAttributeFunction, nil } //Outputs: // -bool: Profile found // -int: Profile version // -[28]byte: Profile hash // -[]byte: Profile bytes // -map[int]messagepack.RawMessage: Raw profile map // -error func GetMyNewestBroadcastProfile(myIdentityHash [16]byte, networkType byte)(bool, int, [28]byte, []byte, map[int]messagepack.RawMessage, error){ identityExists, _, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } if (identityExists == false){ return false, 0, [28]byte{}, nil, nil, errors.New("GetMyNewestBroadcastProfile called with identity that is not mine.") } isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, 0, [28]byte{}, nil, nil, errors.New("GetMyNewestBroadcastProfile called with invalid networkType: " + networkTypeString) } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } networkTypeString := helpers.ConvertByteToString(networkType) networkTypeFoldername := "Network" + networkTypeString profilesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles", networkTypeFoldername) broadcastProfilesList, err := localFilesystem.GetAllFilesInFolderAsList(profilesFolderPath) if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } anyProfileFound := false newestProfileVersion := 0 var newestProfileHash [28]byte newestProfileBytes := make([]byte, 0) newestProfileRawProfileMap := make(map[int]messagepack.RawMessage) newestProfileBroadcastTime := int64(0) for _, profileBytes := range broadcastProfilesList{ ableToRead, profileHash, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(true, profileBytes) if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } if (ableToRead == false){ return false, 0, [28]byte{}, nil, nil, errors.New("MyBroadcasts contains invalid profile.") } if (profileNetworkType != networkType){ return false, 0, [28]byte{}, nil, nil, errors.New("MyBroadcasts contains profile for different networkType.") } if (profileIdentityHash != myIdentityHash){ continue } if (anyProfileFound == false || profileBroadcastTime > newestProfileBroadcastTime){ anyProfileFound = true newestProfileVersion = profileVersion newestProfileHash = profileHash newestProfileBytes = profileBytes newestProfileRawProfileMap = rawProfileMap newestProfileBroadcastTime = profileBroadcastTime } } if (anyProfileFound == false){ return false, 0, [28]byte{}, nil, nil, nil } return true, newestProfileVersion, newestProfileHash, newestProfileBytes, newestProfileRawProfileMap, nil } // This function overwrites an identity's existing broadcast profile with current exported profile //Outputs: // -bool: My identity exists // -[28]byte: Profile hash of new broadcast profile // -error func UpdateMyBroadcastProfile(myIdentityType string, networkType byte)(bool, [28]byte, error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return false, [28]byte{}, errors.New("UpdateMyBroadcastProfile called with invalid networkType: " + networkTypeString) } myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) if (err != nil) { return false, [28]byte{}, err } if (myIdentityExists == false){ return false, [28]byte{}, nil } newProfileFound, exportProfileHash, newProfileBytes, _, err := myProfileExports.GetMyExportedProfile(myIdentityType, networkType) if (err != nil) { return false, [28]byte{}, err } if (newProfileFound == false){ return false, [28]byte{}, errors.New("UpdateMyBroadcastProfile called when export profile is missing.") } ableToRead, newProfileHash, _, newProfileNetworkType, profileIdentityHash, newProfileBroadcastTime, _, newProfileRawProfileMap, err := readProfiles.ReadProfileAndHash(true, newProfileBytes) if (err != nil) { return false, [28]byte{}, err } if (ableToRead == false){ return false, [28]byte{}, errors.New("MyExports contains invalid profile.") } if (newProfileNetworkType != networkType){ return false, [28]byte{}, errors.New("GetMyExportedProfile returning profile with different networkType.") } if (exportProfileHash != newProfileHash){ return false, [28]byte{}, errors.New("GetMyExportedProfile returning different profileHash than the profileBytes") } if (profileIdentityHash != myIdentityHash){ return false, [28]byte{}, errors.New("New profile identity hash is not mine.") } checkIfProfileHasChatKeys := func()(bool, error){ profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "Disabled") if (err != nil) { return false, err } if (profileIsDisabled == true){ return false, nil } if (myIdentityType == "Mate" || myIdentityType == "Moderator"){ return true, nil } return false, nil } profileHasChatKeys, err := checkIfProfileHasChatKeys() if (err != nil) { return false, [28]byte{}, err } if (profileHasChatKeys == true){ // We deal with updating the user's latest chat keys update time // We keep track of this locally and send it within all of our chat messages // The exported profile's chat keys latest update time will be accurate, based on our existing chat keys exists, newProfileChatKeysLatestUpdateTimeString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "ChatKeysLatestUpdateTime") if (err != nil) { return false, [28]byte{}, err } if (exists == false){ return false, [28]byte{}, errors.New("Invalid exported profile: Missing ChatKeysLatestUpdateTime") } newProfileChatKeysLatestUpdateTime, err := helpers.ConvertStringToInt64(newProfileChatKeysLatestUpdateTimeString) if (err != nil) { return false, [28]byte{}, errors.New("Invalid exported profile: Contains invalid ChatKeysLatestUpdateTime: " + newProfileChatKeysLatestUpdateTimeString) } getLatestChatKeysTimeNeedsUpdateBool := func()(bool, error){ latestUpdateTimeExists, existingChatKeysLatestUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, networkType) if (err != nil) { return false, err } if (latestUpdateTimeExists == false){ // No time exists, we need to update it. return true, nil } if (newProfileChatKeysLatestUpdateTime > existingChatKeysLatestUpdateTime){ // The profile we are broadcasting has new chat keys. return true, nil } return false, nil } latestChatKeysTimeNeedsUpdate, err := getLatestChatKeysTimeNeedsUpdateBool() if (err != nil) { return false, [28]byte{}, err } if (latestChatKeysTimeNeedsUpdate == true){ err := myChatKeys.SetMyChatKeysLatestUpdateTime(myIdentityHash, networkType, newProfileBroadcastTime) if (err != nil) { return false, [28]byte{}, err } } exists, newProfileNaclKeyString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "NaclKey") if (err != nil) { return false, [28]byte{}, err } if (exists == false) { return false, [28]byte{}, errors.New("Invalid export profile: Missing NaclKey") } exists, newProfileKyberKeyString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "KyberKey") if (err != nil) { return false, [28]byte{}, err } if (exists == false) { return false, [28]byte{}, errors.New("Invalid export profile: Missing KyberKey") } newProfileNaclKey, err := nacl.ReadNaclPublicKeyString(newProfileNaclKeyString) if (err != nil){ return false, [28]byte{}, errors.New("Invalid export profile: Contains invalid NaclKey: " + newProfileNaclKeyString) } newProfileKyberKey, err := kyber.ReadKyberPublicKeyString(newProfileKyberKeyString) if (err != nil){ return false, [28]byte{}, errors.New("Invalid export profile: Contains invalid KyberKey: " + newProfileKyberKeyString) } // These keys may not be different from the existing keys we have saved // We dont have to check if they are different, we will set them anyway err = myChatKeys.SetMyNewestBroadcastPublicChatKeys(myIdentityHash, networkType, newProfileNaclKey, newProfileKyberKey) if (err != nil) { return false, [28]byte{}, err } } err = DeleteMyBroadcastProfiles(myIdentityHash) if (err != nil) { return false, [28]byte{}, err } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return false, [28]byte{}, err } networkTypeString := helpers.ConvertByteToString(networkType) networkTypeFoldername := "Network" + networkTypeString profilesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles", networkTypeFoldername) newProfileHashHex := encoding.EncodeBytesToHexString(newProfileHash[:]) filename := newProfileHashHex + ".messagepack" err = localFilesystem.CreateOrOverwriteFile(newProfileBytes, profilesFolderPath, filename) if (err != nil) { return false, [28]byte{}, err } // We add the profile to our database // For moderators, this will help with determining moderation details, such as what is banned/approved // For hosts, this will broadcast our profile during our standard host profile seeding tasks // // TODO: Skip/delay this step for Mate users? // For Mate users, this could reveal their identity if the user fulfills their own criteria // For example, a user who requests to download profiles after being offline for a while could reveal their identity. // 1. User makes a request to download profiles which fulfill their criteria within the host's range // 2. Host offers many profiles, some of which have been updated since the user last connected to network // 3. User requests to download all profiles which are newer than a given time EXCEPT for their own profile // The user has a newer version of their own profile which the user has recently broadcasted // The host has not received the profile yet via network propagation. wellFormed, _, err := profileStorage.AddUserProfile(newProfileBytes) if (err != nil) { return false, [28]byte{}, err } if (wellFormed == false){ return false, [28]byte{}, errors.New("Profile to broadcast is not well formed after being well formed already.") } return true, newProfileHash, nil } // This function will delete all broadcast profiles for a provided identity hash func DeleteMyBroadcastProfiles(myIdentityHash [16]byte)error{ isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") if (err != nil) { return err } if (isValid == false){ myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) return errors.New("DeleteMyBroadcastProfiles called with invalid identityHash: " + myIdentityHashHex) } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } profilesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles") network1ProfilesFolderpath := goFilepath.Join(profilesFolderPath, "Network1") network2ProfilesFolderpath := goFilepath.Join(profilesFolderPath, "Network2") deleteMyProfilesInFolder := func(folderPath string, networkType byte)error{ fileList, err := os.ReadDir(folderPath) if (err != nil) { return err } for _, fileObject := range fileList{ fileName := fileObject.Name() filePath := goFilepath.Join(folderPath, fileName) fileBytes, err := os.ReadFile(filePath) if (err != nil){ return err } ableToRead, _, profileNetworkType, profileAuthor, _, _, _, err := readProfiles.ReadProfile(true, fileBytes) if (err != nil) { return err } if (ableToRead == false){ return errors.New("MyBroadcasts malformed: Contains invalid profile.") } if (profileNetworkType != networkType){ return errors.New("MyBroadcasts malformed: Contains profile from different networkType.") } if (profileAuthor != myIdentityHash){ continue } err = os.Remove(filePath) if (err != nil) { return err } } return nil } err = deleteMyProfilesInFolder(network1ProfilesFolderpath, 1) if (err != nil) { return err } err = deleteMyProfilesInFolder(network2ProfilesFolderpath, 2) if (err != nil) { return err } return nil } func BroadcastMyMessage(messageBytes []byte)error{ ableToRead, messageHash, _, messageNetworkType, _, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(true, messageBytes) if (err != nil) { return err } if (ableToRead == false){ return errors.New("BroadcastMyMessage called with invalid message.") } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { return err } if (appNetworkType != messageNetworkType){ return errors.New("BroadcastMyMessage called with message for different networkType than application.") } messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) networkTypeFoldername := "Network" + messageNetworkTypeString messagesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Messages", networkTypeFoldername) messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) filename := messageHashHex + ".messagepack" err = localFilesystem.CreateOrOverwriteFile(messageBytes, messagesFolderPath, filename) if (err != nil) { return err } return nil } // This function should only be called via the myReviews.CreateAndBroadcastMyReview function func BroadcastMyReview(newReview []byte)error{ myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") if (err != nil) { return err } if (myIdentityExists == false) { return errors.New("Trying to broadcast review when my moderator identity does not exist.") } ableToRead, newReviewHash, _, newReviewNetworkType, reviewerIdentityHash, _, newReviewType, newReviewedHash, _, _, err := readReviews.ReadReviewAndHash(true, newReview) if (err != nil) { return err } if (ableToRead == false){ return errors.New("Trying to broadcast invalid review.") } if (myIdentityHash != reviewerIdentityHash){ return errors.New("Trying to broadcast review not created by current moderator identity.") } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } newReviewNetworkTypeString := helpers.ConvertByteToString(newReviewNetworkType) networkTypeFoldername := "Network" + newReviewNetworkTypeString reviewTypeFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", newReviewType, networkTypeFoldername) newReviewHashHex := encoding.EncodeBytesToHexString(newReviewHash[:]) newReviewFilename := newReviewHashHex + ".messagepack" err = localFilesystem.CreateOrOverwriteFile(newReview, reviewTypeFolderPath, newReviewFilename) if (err != nil) { return err } wellFormed, err := reviewStorage.AddReview(newReview) if (err != nil) { return err } if (wellFormed == false){ return errors.New("New review to broadcast is not well formed after being verified already.") } if (newReviewType == "Profile"){ if (len(newReviewedHash) != 28){ reviewedHashHex := encoding.EncodeBytesToHexString(newReviewedHash) return errors.New("ReadReview returning invalid length reviewedHash for profile review: " + reviewedHashHex) } newReviewedProfileHash := [28]byte(newReviewedHash) err = mySkippedContent.DeleteProfileFromMySkippedProfilesMap(newReviewedProfileHash) if (err != nil) { return err } } else if (newReviewType == "Attribute"){ if (len(newReviewedHash) != 27){ reviewedHashHex := encoding.EncodeBytesToHexString(newReviewedHash) return errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex) } newReviewedAttributeHash := [27]byte(newReviewedHash) err = mySkippedContent.DeleteAttributeFromMySkippedAttributesMap(newReviewedAttributeHash) if (err != nil) { return err } } else if (newReviewType == "Message"){ if (len(newReviewedHash) != 26){ reviewedHashHex := encoding.EncodeBytesToHexString(newReviewedHash) return errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex) } newReviewedMessageHash := [26]byte(newReviewedHash) err = mySkippedContent.DeleteMessageFromMySkippedMessagesMap(newReviewedMessageHash) if (err != nil) { return err } } return nil } // This function will prune old reviews that have been replaced by new reviews // For example, if we approved a message, then banned the same message later, we need to prune the approve review from our broadcasts func PruneMyBroadcastedReviews()error{ //TODO // If our moderator identity does not exist, delete all of our revies // We also need to deal with the reality that full profile approve verdicts replace attribute ban verdicts // We also need to deal with the reality that attribute ban verdicts replace full profile approve verdicts // We have to delete all reviews that are not the newest. // None reviews must be kept, when they are not replaced by other reviews return nil }