From 49e0d849fa3e1a56cb5b581d6d033e8aa400bce6 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 21 Feb 2023 15:55:14 -0800 Subject: [PATCH] Large API Refactor in prep for autobindings --- app/app.go | 43 +++++++--- event/common.go | 3 +- .../filesharing/filesharing_functionality.go | 67 +++++++++++++++ model/constants/attributes.go | 2 + peer/cwtch_peer.go | 86 ++++++++++++++++++- peer/profile_interface.go | 26 +++++- testing/cwtch_peer_server_integration_test.go | 14 +-- .../encrypted_storage_integration_test.go | 10 +-- .../file_sharing_integration_test.go | 12 +-- 9 files changed, 230 insertions(+), 33 deletions(-) diff --git a/app/app.go b/app/app.go index 32b5e33..5398518 100644 --- a/app/app.go +++ b/app/app.go @@ -38,11 +38,10 @@ type application struct { // Application is a full cwtch peer application. It allows management, usage and storage of multiple peers type Application interface { LoadProfiles(password string) - CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) - // Deprecated in 1.10 - CreateTaggedPeer(name string, password string, tag string) + CreateProfile(name string, password string, autostart bool) + ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error) - DeletePeer(onion string, currentPassword string) + DeleteProfile(onion string, currentPassword string) AddPeerPlugin(onion string, pluginID plugins.PluginID) GetPrimaryBus() event.Manager @@ -51,7 +50,7 @@ type Application interface { QueryACNVersion() ActivateEngines(doListn, doPeers, doServers bool) - ActivatePeerEngine(onion string, doListen, doPeers, doServers bool) + ActivatePeerEngine(onion string) DeactivatePeerEngine(onion string) ReadSettings() GlobalSettings @@ -67,8 +66,7 @@ type Application interface { // LoadProfileFn is the function signature for a function in an app that loads a profile type LoadProfileFn func(profile peer.CwtchPeer) -// NewApp creates a new app with some environment awareness and initializes a Tor Manager -func NewApp(acn connectivity.ACN, appDirectory string) Application { +func InitApp(appDirectory string) *GlobalSettingsFile { log.Debugf("NewApp(%v)\n", appDirectory) os.MkdirAll(path.Join(appDirectory, "profiles"), 0700) @@ -79,6 +77,11 @@ func NewApp(acn connectivity.ACN, appDirectory string) Application { if err != nil { log.Errorf("error initializing global settings file %. Global settings might not be loaded or saves", err) } + return settings +} + +// NewApp creates a new app with some environment awareness and initializes a Tor Manager +func NewApp(acn connectivity.ACN, appDirectory string, settings *GlobalSettingsFile) Application { app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager(), settings: settings} app.peers = make(map[string]peer.CwtchPeer) @@ -159,6 +162,22 @@ func (ap *application) AddPlugin(peerid string, id plugins.PluginID, bus event.M } } +func (app *application) CreateProfile(name string, password string, autostart bool) { + autostartVal := constants.True + if !autostart { + autostartVal = constants.False + } + tagVal := constants.ProfileTypeV1Password + if password == DefactoPasswordForUnencryptedProfiles { + tagVal = constants.ProfileTypeV1DefaultPassword + } + + app.CreatePeer(name, password, map[attr.ZonedPath]string{ + attr.ProfileZone.ConstructZonedPath(constants.Tag): tagVal, + attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal, + }) +} + // Deprecated in 1.10 func (app *application) CreateTaggedPeer(name string, password string, tag string) { app.CreatePeer(name, password, map[attr.ZonedPath]string{attr.ProfileZone.ConstructZonedPath(constants.Tag): tag}) @@ -192,8 +211,8 @@ func (app *application) CreatePeer(name string, password string, attributes map[ app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True})) } -func (app *application) DeletePeer(onion string, password string) { - log.Debugf("DeletePeer called on %v\n", onion) +func (app *application) DeleteProfile(onion string, password string) { + log.Debugf("DeleteProfile called on %v\n", onion) app.appmutex.Lock() defer app.appmutex.Unlock() @@ -333,7 +352,7 @@ func (app *application) ActivateEngines(doListen, doPeers, doServers bool) { } // ActivePeerEngine creates a peer engine for use with an ACN, should be called once the underlying ACN is online -func (app *application) ActivatePeerEngine(onion string, doListen, doPeers, doServers bool) { +func (app *application) ActivatePeerEngine(onion string) { profile := app.GetPeer(onion) if profile != nil { if _, exists := app.engines[onion]; !exists { @@ -341,10 +360,10 @@ func (app *application) ActivatePeerEngine(onion string, doListen, doPeers, doSe app.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated)) app.QueryACNStatus() - if doListen { + if true { profile.Listen() } - profile.StartConnections(doPeers, doServers) + profile.StartConnections(true, true) } } } diff --git a/event/common.go b/event/common.go index abb6976..c9041b2 100644 --- a/event/common.go +++ b/event/common.go @@ -228,7 +228,8 @@ type Field string const ( // A peers local onion address - Onion = Field("Onion") + Onion = Field("Onion") + ProfileOnion = Field("ProfileOnion") RemotePeer = Field("RemotePeer") LastSeen = Field("LastSeen") diff --git a/functionality/filesharing/filesharing_functionality.go b/functionality/filesharing/filesharing_functionality.go index 87c87a4..970d7c8 100644 --- a/functionality/filesharing/filesharing_functionality.go +++ b/functionality/filesharing/filesharing_functionality.go @@ -178,6 +178,65 @@ func (om *OverlayMessage) ShouldAutoDL() bool { return false } +func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversation int, fileKey string) { + if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", fileKey)); exists { + if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists { + log.Infof("resuming %s", fileKey) + f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, files.MaxManifestSize*files.DefaultChunkSize) + } else { + log.Errorf("found manifest path but not download path for %s", fileKey) + } + } else { + log.Errorf("no stored manifest path found for %s", fileKey) + } +} + +func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) { + path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)) + if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True { + profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{ + event.ProfileOnion: profile.GetOnion(), + event.FileKey: fileKey, + event.FilePath: path, + event.TempFile: "", + })) + } else { + log.Infof("CheckDownloadStatus found .path but not .complete") + profile.PublishEvent(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{ + event.ProfileOnion: profile.GetOnion(), + event.FileKey: fileKey, + event.Progress: "-1", + event.FileSizeInChunks: "-1", + event.FilePath: path, + })) + } +} + +func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string { + fileKey, overlay, err := f.ShareFile(sharefilepath, profile) + if err != nil { + log.Errorf("error sharing file: %v", err) + } else if conversationID == -1 { + // FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume + // there is only one, and it is the custom profile image... + profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey, fileKey) + } else { + // Set a new attribute so we can associate this download with this conversation... + profile.SetConversationAttribute(conversationID, attr.ConversationScope.ConstructScopedZonedPath(attr.FilesharingZone.ConstructZonedPath(fileKey)), "") + id, err := profile.SendMessage(conversationID, overlay) + if err == nil { + return profile.EnhancedGetMessageById(conversationID, id) + } + } + return "" +} + +// DownloadFileDefaultLimit given a profile, a conversation handle and a file sharing key, start off a download process +// to downloadFilePath with a default filesize limit +func (f *Functionality) DownloadFileDefaultLimit(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) error { + return f.DownloadFile(profile, conversationID, downloadFilePath, manifestFilePath, key, files.MaxManifestSize*files.DefaultChunkSize) +} + // DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process // to downloadFilePath func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string, limit uint64) error { @@ -409,6 +468,14 @@ type SharedFile struct { Expired bool } +func (f *Functionality) EnhancedGetSharedFiles(profile peer.CwtchPeer, conversationID int) string { + data, err := json.Marshal(f.GetSharedFiles(profile, conversationID)) + if err == nil { + return string(data) + } + return "" +} + // GetSharedFiles returns all file shares associated with a given conversation func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile { sharedFiles := []SharedFile{} diff --git a/model/constants/attributes.go b/model/constants/attributes.go index 9e6f4f6..e498222 100644 --- a/model/constants/attributes.go +++ b/model/constants/attributes.go @@ -56,3 +56,5 @@ const SyncPreLastMessageTime = "SyncPreLastMessageTime" const SyncMostRecentMessageTime = "SyncMostRecentMessageTime" const AttrLastConnectionTime = "last-connection-time" +const PeerAutostart = "autostart" +const Archived = "archived" diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 88aef1a..8375d41 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -75,6 +75,90 @@ type cwtchPeer struct { experimentsLock sync.Mutex } +func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count int) string { + var emessages []EnhancedMessage = make([]EnhancedMessage, count) + + messages, err := cp.GetMostRecentMessages(conversation, 0, index, count) + if err == nil { + + for i, message := range messages { + + time, _ := time.Parse(time.RFC3339Nano, message.Attr[constants.AttrSentTimestamp]) + emessages[i].Message = model.Message{ + Message: message.Body, + Acknowledged: message.Attr[constants.AttrAck] == constants.True, + Error: message.Attr[constants.AttrErr], + PeerID: message.Attr[constants.AttrAuthor], + Timestamp: time, + } + emessages[i].ID = message.ID + emessages[i].Attributes = message.Attr + emessages[i].ContentHash = model.CalculateContentHash(message.Attr[constants.AttrAuthor], message.Body) + } + } + + bytes, _ := json.Marshal(emessages) + return string(bytes) +} + +func (cp *cwtchPeer) EnhancedGetMessageById(conversation int, messageID int) string { + var message EnhancedMessage + dbmessage, attr, err := cp.GetChannelMessage(conversation, 0, messageID) + if err == nil { + time, _ := time.Parse(time.RFC3339Nano, attr[constants.AttrSentTimestamp]) + message.Message = model.Message{ + Message: dbmessage, + Acknowledged: attr[constants.AttrAck] == constants.True, + Error: attr[constants.AttrErr], + PeerID: attr[constants.AttrAuthor], + Timestamp: time, + } + message.ID = messageID + message.Attributes = attr + message.ContentHash = model.CalculateContentHash(attr[constants.AttrAuthor], dbmessage) + } + bytes, _ := json.Marshal(message) + return string(bytes) +} + +func (cp *cwtchPeer) EnhancedGetMessageByContentHash(conversation int, contentHash string) string { + var message EnhancedMessage + offset, err := cp.GetChannelMessageByContentHash(conversation, 0, contentHash) + if err == nil { + messages, err := cp.GetMostRecentMessages(conversation, 0, offset, 1) + if err == nil { + time, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants.AttrSentTimestamp]) + message.Message = model.Message{ + Message: messages[0].Body, + Acknowledged: messages[0].Attr[constants.AttrAck] == constants.True, + Error: messages[0].Attr[constants.AttrErr], + PeerID: messages[0].Attr[constants.AttrAuthor], + Timestamp: time, + } + message.ID = messages[0].ID + message.Attributes = messages[0].Attr + message.LocalIndex = offset + message.ContentHash = contentHash + } else { + log.Errorf("error fetching local index {} ", err) + } + } + bytes, _ := json.Marshal(message) + return string(bytes) +} + +func (cp *cwtchPeer) EnhancedSendMessage(conversation int, message string) string { + mid, err := cp.SendMessage(conversation, message) + if err == nil { + return cp.EnhancedGetMessageById(conversation, mid) + } + return "" +} + +func (cp *cwtchPeer) ArchiveConversation(conversationID int) { + cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Archived)), constants.True) +} + // IsFeatureEnabled returns true if the functionality defined by featureName has been enabled by the application, false otherwise. // this function is intended to be used by ProfileHooks to determine if they should execute experimental functionality. func (cp *cwtchPeer) IsFeatureEnabled(featureName string) bool { @@ -119,7 +203,7 @@ func (cp *cwtchPeer) StoreCachedTokens(tokenServer string, tokens []*privacypass } } -func (cp *cwtchPeer) Export(file string) error { +func (cp *cwtchPeer) ExportProfile(file string) error { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.Export(file) diff --git a/peer/profile_interface.go b/peer/profile_interface.go index 98c463d..4a22052 100644 --- a/peer/profile_interface.go +++ b/peer/profile_interface.go @@ -48,6 +48,10 @@ type ModifyServers interface { // SendMessages enables a caller to sender messages to a contact type SendMessages interface { SendMessage(conversation int, message string) (int, error) + + // EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database + EnhancedSendMessage(conversation int, message string) string + SendInviteToConversation(conversationID int, inviteConversationID int) (int, error) SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string) } @@ -105,6 +109,7 @@ type CwtchPeer interface { // New Unified Conversation Interfaces NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error) FetchConversations() ([]*model.Conversation, error) + ArchiveConversation(conversation int) GetConversationInfo(conversation int) (*model.Conversation, error) FetchConversationInfo(handle string) (*model.Conversation, error) AcceptConversation(conversation int) error @@ -121,6 +126,15 @@ type CwtchPeer interface { GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error) UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error + // EnhancedGetMessageById returns a json-encoded enhanced message, suitable for rendering in a UI + EnhancedGetMessageById(conversation int, mid int) string + + // EnhancedGetMessageByContentHash returns a json-encoded enhanced message, suitable for rendering in a UI + EnhancedGetMessageByContentHash(conversation int, hash string) string + + // EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI + EnhancedGetMessages(conversation int, index int, count int) string + // Server Token APIS // TODO move these to feature protected interfaces StoreCachedTokens(tokenServer string, tokens []*privacypass.Token) @@ -128,10 +142,20 @@ type CwtchPeer interface { // Profile Management CheckPassword(password string) bool ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error - Export(file string) error + ExportProfile(file string) error Delete() PublishEvent(resp event.Event) RegisterHook(hook ProfileHooks) UpdateExperiments(enabled bool, experiments map[string]bool) IsFeatureEnabled(featureName string) bool } + +// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI. +type EnhancedMessage struct { + model.Message + ID int // the actual ID of the message in the database (not the row number) + LocalIndex int // local index in the DB (row #). Can be empty (most calls supply it) but lookup by hash will fill it + ContentHash string + ContactImage string + Attributes map[string]string +} diff --git a/testing/cwtch_peer_server_integration_test.go b/testing/cwtch_peer_server_integration_test.go index 9845b81..18e92cc 100644 --- a/testing/cwtch_peer_server_integration_test.go +++ b/testing/cwtch_peer_server_integration_test.go @@ -139,7 +139,7 @@ func TestCwtchPeerIntegration(t *testing.T) { const ServerAddr = "nfhxzvzxinripgdh4t2m4xcy3crf6p4cbhectgckuj3idsjsaotgowad" serverKeyBundle, _ := base64.StdEncoding.DecodeString(ServerKeyBundleBase64) - app := app2.NewApp(acn, "./storage") + app := app2.NewApp(acn, "./storage", app2.InitApp("./storage")) usr, _ := user.Current() cwtchDir := path.Join(usr.HomeDir, ".cwtch") @@ -152,31 +152,31 @@ func TestCwtchPeerIntegration(t *testing.T) { // ***** cwtchPeer setup ***** log.Infoln("Creating Alice...") - app.CreateTaggedPeer("Alice", "asdfasdf", "test") + app.CreateProfile("Alice", "asdfasdf", true) log.Infoln("Creating Bob...") - app.CreateTaggedPeer("Bob", "asdfasdf", "test") + app.CreateProfile("Bob", "asdfasdf", true) log.Infoln("Creating Carol...") - app.CreateTaggedPeer("Carol", "asdfasdf", "test") + app.CreateProfile("Carol", "asdfasdf", true) alice := app2.WaitGetPeer(app, "Alice") aliceBus := app.GetEventBus(alice.GetOnion()) - app.ActivatePeerEngine(alice.GetOnion(), true, true, true) + app.ActivatePeerEngine(alice.GetOnion()) log.Infoln("Alice created:", alice.GetOnion()) alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice") alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer}) bob := app2.WaitGetPeer(app, "Bob") bobBus := app.GetEventBus(bob.GetOnion()) - app.ActivatePeerEngine(bob.GetOnion(), true, true, true) + app.ActivatePeerEngine(bob.GetOnion()) log.Infoln("Bob created:", bob.GetOnion()) bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob") bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer}) carol := app2.WaitGetPeer(app, "Carol") carolBus := app.GetEventBus(carol.GetOnion()) - app.ActivatePeerEngine(carol.GetOnion(), true, true, true) + app.ActivatePeerEngine(carol.GetOnion()) log.Infoln("Carol created:", carol.GetOnion()) carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol") carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer}) diff --git a/testing/encryptedstorage/encrypted_storage_integration_test.go b/testing/encryptedstorage/encrypted_storage_integration_test.go index 27bf862..225d178 100644 --- a/testing/encryptedstorage/encrypted_storage_integration_test.go +++ b/testing/encryptedstorage/encrypted_storage_integration_test.go @@ -59,9 +59,9 @@ func TestEncryptedStorage(t *testing.T) { defer acn.Close() acn.WaitTillBootstrapped() - app := app2.NewApp(acn, cwtchDir) - app.CreateTaggedPeer("alice", "password", constants.ProfileTypeV1Password) - app.CreateTaggedPeer("bob", "password", constants.ProfileTypeV1Password) + app := app2.NewApp(acn, cwtchDir, app2.InitApp(cwtchDir)) + app.CreateProfile("alice", "password", true) + app.CreateProfile("bob", "password", true) alice := app2.WaitGetPeer(app, "alice") bob := app2.WaitGetPeer(app, "bob") @@ -130,7 +130,7 @@ func TestEncryptedStorage(t *testing.T) { t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages) } - err = alice.Export("alice.tar.gz") + err = alice.ExportProfile("alice.tar.gz") if err != nil { t.Fatalf("could not export profile: %v", err) } @@ -140,7 +140,7 @@ func TestEncryptedStorage(t *testing.T) { t.Fatal("profile is already imported...this should fail") } - app.DeletePeer(alice.GetOnion(), "password") + app.DeleteProfile(alice.GetOnion(), "password") alice, err = app.ImportProfile("alice.tar.gz", "password") if err != nil { t.Fatalf("profile should have successfully imported: %s", err) diff --git a/testing/filesharing/file_sharing_integration_test.go b/testing/filesharing/file_sharing_integration_test.go index a438868..e3cabee 100644 --- a/testing/filesharing/file_sharing_integration_test.go +++ b/testing/filesharing/file_sharing_integration_test.go @@ -97,7 +97,7 @@ func TestFileSharing(t *testing.T) { acn.WaitTillBootstrapped() defer acn.Close() - app := app2.NewApp(acn, "./storage") + app := app2.NewApp(acn, "./storage", app2.InitApp("./storage")) usr, _ := user.Current() cwtchDir := path.Join(usr.HomeDir, ".cwtch") @@ -106,16 +106,16 @@ func TestFileSharing(t *testing.T) { os.Mkdir(path.Join(cwtchDir, "testing"), 0700) t.Logf("Creating Alice...") - app.CreateTaggedPeer("alice", "asdfasdf", "testing") + app.CreateProfile("alice", "asdfasdf", true) t.Logf("Creating Bob...") - app.CreateTaggedPeer("bob", "asdfasdf", "testing") + app.CreateProfile("bob", "asdfasdf", true) t.Logf("** Waiting for Alice, Bob...") alice := app2.WaitGetPeer(app, "alice") - app.ActivatePeerEngine(alice.GetOnion(), true, true, true) + app.ActivatePeerEngine(alice.GetOnion()) bob := app2.WaitGetPeer(app, "bob") - app.ActivatePeerEngine(bob.GetOnion(), true, true, true) + app.ActivatePeerEngine(bob.GetOnion()) alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) @@ -178,7 +178,7 @@ func TestFileSharing(t *testing.T) { testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle) // test that we can delete bob... - app.DeletePeer(bob.GetOnion(), "asdfasdf") + app.DeleteProfile(bob.GetOnion(), "asdfasdf") queueOracle.Shutdown() app.Shutdown()