Large API Refactor in prep for autobindings

This commit is contained in:
Sarah Jamie Lewis 2023-02-21 15:55:14 -08:00
parent 861390b11d
commit 0e49d70d65
9 changed files with 230 additions and 33 deletions

View File

@ -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)
}
}
}

View File

@ -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")

View File

@ -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{}

View File

@ -56,3 +56,5 @@ const SyncPreLastMessageTime = "SyncPreLastMessageTime"
const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
const AttrLastConnectionTime = "last-connection-time"
const PeerAutostart = "autostart"
const Archived = "archived"

View File

@ -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)

View File

@ -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
}

View File

@ -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})

View File

@ -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)

View File

@ -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()