Support Save History Default + Delete Server #529

Merged
sarah merged 8 commits from stable-blockers into master 2023-08-22 20:22:23 +00:00
8 changed files with 79 additions and 19 deletions

View File

@ -327,19 +327,25 @@ const (
ContextSendFile = "im.cwtch.file.send.chunk" ContextSendFile = "im.cwtch.file.send.chunk"
) )
// Define Default Attribute Keys // Define Attribute Keys related to history preservation
const ( const (
SaveHistoryKey = "SavePeerHistory" PreserveHistoryDefaultSettingKey = "SaveHistoryDefault" // profile level default
SaveHistoryKey = "SavePeerHistory" // peer level setting
) )
// Define Default Attribute Values // Define Default Attribute Values
const ( const (
// Save History has 3 distinct states. By default we don't save history (DefaultDeleteHistory), if the peer confirms this // Save History has 3 distinct states. By default we refer to the profile level
// we change to DeleteHistoryConfirmed, if they confirm they want to save then this becomes SaveHistoryConfirmed // attribute PreserveHistoryDefaultSettingKey ( default: false i.e. DefaultDeleteHistory),
// We use this distinction between default and confirmed to drive UI // For each contact, if the profile owner confirms deletion we change to DeleteHistoryConfirmed,
DeleteHistoryDefault = "DefaultDeleteHistory" // if the profile owner confirms they want to save history then this becomes SaveHistoryConfirmed
// These settings are set at the UI level using Get/SetScopeZoneAttribute with scoped zone: local.profile.*
SaveHistoryConfirmed = "SaveHistory" SaveHistoryConfirmed = "SaveHistory"
DeleteHistoryConfirmed = "DeleteHistoryConfirmed" DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
// NOTE: While this says "[DeleteHistory]Default", The actual behaviour will now depend on the
// global app/profile value of PreserveHistoryDefaultSettingKey
DeleteHistoryDefault = "DefaultDeleteHistory"
) )
// Bool strings // Bool strings

View File

@ -101,6 +101,21 @@ func (f *Functionality) GetServerInfoList(profile peer.CwtchPeer) []Server {
return servers return servers
} }
// DeleteServer purges a server and all related keys from a profile
func (f *Functionality) DeleteServerInfo(profile peer.CwtchPeer, serverOnion string) error {
// Servers are stores as special conversations
ci, err := profile.FetchConversationInfo(serverOnion)
if err != nil {
return err
}
// Purge keys...
Review

what's the ui flow for this? will the UI warn / prompt about groups and delete them first if asked? do we have a way to signal a group is now serverless and will never be online?

what's the ui flow for this? will the UI warn / prompt about groups and delete them first if asked? do we have a way to signal a group is now serverless and will never be online?
Review

The main usecase for this right now is people cleaning up servers they no longer use. To that end we can just disable the button on the UI side if there are active groups.

The main usecase for this right now is people cleaning up servers they no longer use. To that end we can just disable the button on the UI side if there are active groups.
// NOTE: This will leave some groups in the state of being unable to connect to a particular
// server.
profile.DeleteConversation(ci.ID)
f.PublishServerUpdate(profile)
return nil
}
// GetServerInfo compiles all the information the UI might need regarding a particular server including any verified // GetServerInfo compiles all the information the UI might need regarding a particular server including any verified
// cryptographic keys // cryptographic keys
func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string) Server { func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string) Server {

4
go.mod
View File

@ -4,7 +4,7 @@ go 1.17
require ( require (
git.openprivacy.ca/cwtch.im/tapir v0.6.0 git.openprivacy.ca/cwtch.im/tapir v0.6.0
git.openprivacy.ca/openprivacy/connectivity v1.8.6 git.openprivacy.ca/openprivacy/connectivity v1.11.0
git.openprivacy.ca/openprivacy/log v1.0.3 git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 github.com/mutecomm/go-sqlcipher/v4 v4.4.2
@ -15,7 +15,7 @@ require (
require ( require (
filippo.io/edwards25519 v1.0.0 // indirect filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.4 // indirect git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/merlin v0.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect

6
go.sum
View File

@ -3,10 +3,12 @@ filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/cwtch.im/tapir v0.6.0 h1:TtnKjxitkIDMM7Qn0n/u+mOHRLJzuQUYjYRu5n0/QFY= git.openprivacy.ca/cwtch.im/tapir v0.6.0 h1:TtnKjxitkIDMM7Qn0n/u+mOHRLJzuQUYjYRu5n0/QFY=
git.openprivacy.ca/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY= git.openprivacy.ca/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU= git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.8.6 h1:g74PyDGvpMZ3+K0dXy3mlTJh+e0rcwNk0XF8owzkmOA= git.openprivacy.ca/openprivacy/bine v0.0.5 h1:DJs5gqw3SkvLSgRDvroqJxZ7F+YsbxbBRg5t0rU5gYE=
git.openprivacy.ca/openprivacy/bine v0.0.5/go.mod h1:fwdeq6RO08WDkV0k7HfArsjRvurVULoUQmT//iaABZM=
git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg= git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg=
git.openprivacy.ca/openprivacy/connectivity v1.11.0 h1:roASjaFtQLu+HdH5fa2wx6F00NL3YsUTlmXBJh8aLZk=
git.openprivacy.ca/openprivacy/connectivity v1.11.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0= git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw= git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=

View File

@ -194,10 +194,16 @@ func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool
cp.experiments = model.InitExperiments(enabled, experiments) cp.experiments = model.InitExperiments(enabled, experiments)
} }
// NotifySettingsUpdate notifies a Cwtch profile of a change in the nature of global experiments. The Cwtch Profile uses // NotifySettingsUpdate notifies a Cwtch profile of a change in the nature of global settings.
// this information to update registered extensions. // The Cwtch Profile uses this information to update registered extensions in addition
// to updating internal settings.
func (cp *cwtchPeer) NotifySettingsUpdate(settings settings.GlobalSettings) { func (cp *cwtchPeer) NotifySettingsUpdate(settings settings.GlobalSettings) {
log.Debugf("Cwtch Profile Settings Update: %v", settings) log.Debugf("Cwtch Profile Settings Update: %v", settings)
// update the save history default...
cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, event.PreserveHistoryDefaultSettingKey, strconv.FormatBool(settings.DefaultSaveHistory))
// pass these seetings updates
cp.extensionLock.Lock() cp.extensionLock.Lock()
defer cp.extensionLock.Unlock() defer cp.extensionLock.Unlock()
for _, extension := range cp.extensions { for _, extension := range cp.extensions {
@ -981,8 +987,8 @@ func (cp *cwtchPeer) AddServer(serverSpecification string) (string, error) {
// we haven't seen this key associated with the server before // we haven't seen this key associated with the server before
} }
// // If we have gotten to this point we can assume this is a safe key bundle signed by the // If we have gotten to this point we can assume this is a safe key bundle signed by the
// // server with no conflicting keys. So we are going to save all the keys // server with no conflicting keys. So we are going to save all the keys
for k, v := range ab { for k, v := range ab {
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(k)), v) cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(k)), v)
} }
@ -1537,6 +1543,13 @@ func (cp *cwtchPeer) eventHandler() {
} }
case event.PeerStateChange: case event.PeerStateChange:
handle := ev.Data[event.RemotePeer] handle := ev.Data[event.RemotePeer]
// we need to do this first because calls in the rest of this block may result in
// events that result the UI or bindings fetching new data.
cp.mutex.Lock()
cp.state[handle] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED { if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
ci, err := cp.FetchConversationInfo(handle) ci, err := cp.FetchConversationInfo(handle)
var cid int var cid int
@ -1577,9 +1590,6 @@ func (cp *cwtchPeer) eventHandler() {
} }
cp.extensionLock.Unlock() cp.extensionLock.Unlock()
cp.mutex.Lock()
cp.state[ev.Data[event.RemotePeer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
case event.ServerStateChange: case event.ServerStateChange:
cp.mutex.Lock() cp.mutex.Lock()
prevState := cp.state[ev.Data[event.GroupServer]] prevState := cp.state[ev.Data[event.GroupServer]]

View File

@ -13,6 +13,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
) )
@ -835,12 +836,30 @@ func (cps *CwtchProfileStorage) PurgeConversationChannel(conversation int, chann
// PurgeNonSavedMessages deletes all message conversations that are not explicitly set to saved. // PurgeNonSavedMessages deletes all message conversations that are not explicitly set to saved.
func (cps *CwtchProfileStorage) PurgeNonSavedMessages() { func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
// Purge Messages that are not stored...
// check to see if the profile global setting has been explicitly set to save (peer) conversations by default.
defaultSave := false
key, err := cps.LoadProfileKeyValue(TypeAttribute, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.PreserveHistoryDefaultSettingKey)).ToString())
if err == nil {
if defaultSaveSetting, err := strconv.ParseBool(string(key)); err == nil {
defaultSave = defaultSaveSetting
}
}
// For each conversation, all that is not explicitly saved will be lost...
ci, err := cps.FetchConversations() ci, err := cps.FetchConversations()
if err == nil { if err == nil {
for _, conversation := range ci { for _, conversation := range ci {
// unless this is a server or a group...for which we default save always (for legacy reasons)
// FIXME: revisit this for hybrid groups.
if !conversation.IsGroup() && !conversation.IsServer() { if !conversation.IsGroup() && !conversation.IsServer() {
if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed { // Note that we only check for confirmed status here...if it is set to any other value we will fallthrough to the default.
saveHistoryConfirmed := conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] == event.SaveHistoryConfirmed
deleteHistoryConfirmed := conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] == event.DeleteHistoryConfirmed
// we purge conversation history in two specific instances...
// if the conversation has been explicitly marked as delete history confirmed OR
// if save history hasn't been confirmed and default save history is false - i.e. in all other cases
if deleteHistoryConfirmed || (!saveHistoryConfirmed && !defaultSave) {
log.Debugf("purging conversation...") log.Debugf("purging conversation...")
// TODO: At some point in the future this needs to iterate over channels and make a decision for each on.. // TODO: At some point in the future this needs to iterate over channels and make a decision for each on..
cps.PurgeConversationChannel(conversation.ID, 0) cps.PurgeConversationChannel(conversation.ID, 0)

View File

@ -464,6 +464,10 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
e.ignoreOnShutdown(e.serverAuthed)(onion) e.ignoreOnShutdown(e.serverAuthed)(onion)
return return
} }
// if we are not authed or synced then we are stuck...
e.ignoreOnShutdown(e.serverConnecting)(onion)
log.Errorf("server connection attempt issued to active connection")
} }
} }

View File

@ -57,6 +57,7 @@ type GlobalSettings struct {
TorCacheDir string TorCacheDir string
BlodeuweddPath string BlodeuweddPath string
FontScaling float64 FontScaling float64
DefaultSaveHistory bool
} }
var DefaultGlobalSettings = GlobalSettings{ var DefaultGlobalSettings = GlobalSettings{
@ -83,6 +84,7 @@ var DefaultGlobalSettings = GlobalSettings{
TorCacheDir: "", TorCacheDir: "",
BlodeuweddPath: "", BlodeuweddPath: "",
FontScaling: 1.0, // use the system pixel scaling default FontScaling: 1.0, // use the system pixel scaling default
DefaultSaveHistory: false,
} }
func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) { func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) {
@ -131,6 +133,8 @@ func (globalSettingsFile *GlobalSettingsFile) ReadGlobalSettings() GlobalSetting
return settings //firstTime = true return settings //firstTime = true
} }
// note: by giving json.Unmarshal settings we are providing it defacto defaults
// from DefaultGlobalSettings
err = json.Unmarshal(settingsBytes, &settings) err = json.Unmarshal(settingsBytes, &settings)
if err != nil { if err != nil {
log.Errorf("Could not parse global ui settings: %v\n", err) log.Errorf("Could not parse global ui settings: %v\n", err)