seekia/internal/network/myBroadcasts/myBroadcasts.go

602 lines
23 KiB
Go
Raw Normal View History

// 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)
newestProfileCreationTime := int64(0)
for _, profileBytes := range broadcastProfilesList{
ableToRead, profileHash, profileVersion, profileNetworkType, profileIdentityHash, profileCreationTime, _, 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 || profileCreationTime > newestProfileCreationTime){
anyProfileFound = true
newestProfileVersion = profileVersion
newestProfileHash = profileHash
newestProfileBytes = profileBytes
newestProfileRawProfileMap = rawProfileMap
newestProfileCreationTime = profileCreationTime
}
}
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, newProfileCreationTime, _, 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, newProfileCreationTime)
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
}