From fd53fadef951d53e5e41e9183bf57f6abf48d049 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 24 Mar 2021 16:24:42 -0700 Subject: [PATCH] Add Contact Flow --- features/contacts/contact_functionality.go | 41 +++++++ .../contact_functionality_addcontact_test.go | 116 ++++++++++++++++++ features/response.go | 13 ++ go.mod | 4 +- go.sum | 6 + lib.go | 13 ++ utils/eventHandler.go | 6 +- utils/manager.go | 11 +- 8 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 features/contacts/contact_functionality.go create mode 100644 features/contacts/contact_functionality_addcontact_test.go create mode 100644 features/response.go diff --git a/features/contacts/contact_functionality.go b/features/contacts/contact_functionality.go new file mode 100644 index 0000000..1f62219 --- /dev/null +++ b/features/contacts/contact_functionality.go @@ -0,0 +1,41 @@ +package contact + +import ( + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/peer" + "git.openprivacy.ca/flutter/libcwtch-go/features" + "git.openprivacy.ca/openprivacy/connectivity/tor" +) + +// Functionality groups some common UI triggered functions for contacts... +type Functionality struct { +} + +const addContactPrefix = "addcontact" + +const sendMessagePrefix = "sendmessage" + +// FunctionalityGate returns contact.Functionality always +func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) { + return new(Functionality), nil +} + +// SendMessage handles sending messages to contacts +func (pf *Functionality) SendMessage(peer peer.SendMessages, handle string, message string) features.Response { + eventID := peer.SendMessageToPeer(handle, message) + return features.ConstructResponse(sendMessagePrefix, eventID) +} + +// HandleImportString handles contact import strings +func (pf *Functionality) HandleImportString(peer peer.ModifyContactsAndPeers, importString string) features.Response { + if tor.IsValidHostname(importString) { + if peer.GetContact(importString) == nil { + peer.AddContact(importString, importString, model.AuthApproved) + // Implicit Peer Attempt + peer.PeerWithOnion(importString) + return features.ConstructResponse(addContactPrefix, "success") + } + return features.ConstructResponse(addContactPrefix, "contact_already_exists") + } + return features.ConstructResponse(addContactPrefix, "invalid_import_string") +} diff --git a/features/contacts/contact_functionality_addcontact_test.go b/features/contacts/contact_functionality_addcontact_test.go new file mode 100644 index 0000000..2bf96ae --- /dev/null +++ b/features/contacts/contact_functionality_addcontact_test.go @@ -0,0 +1,116 @@ +package contact + +import ( + "cwtch.im/cwtch/model" + "git.openprivacy.ca/flutter/libcwtch-go/features" + "testing" +) + +const ValidHostname = "openpravyvc6spbd4flzn4g2iqu4sxzsizbtb5aqec25t76dnoo5w7yd" + +type MockPeer struct { + hasContact bool + addContact bool + peerRequest bool +} + +func (m MockPeer) GetContacts() []string { + panic("should never be called") +} + +func (m MockPeer) GetContact(s string) *model.PublicProfile { + if m.hasContact { + return &(model.GenerateNewProfile("").PublicProfile) + } + return nil +} + +func (m MockPeer) GetContactAttribute(s string, s2 string) (string, bool) { + panic("should never be called") +} + +func (m *MockPeer) AddContact(nick, onion string, authorization model.Authorization) { + m.addContact = true +} + +func (m MockPeer) SetContactAuthorization(s string, authorization model.Authorization) error { + panic("should never be called") +} + +func (m MockPeer) SetContactAttribute(s string, s2 string, s3 string) { + panic("should never be called") +} + +func (m MockPeer) DeleteContact(s string) { + panic("should never be called") +} + +func (m *MockPeer) PeerWithOnion(s string) { + m.peerRequest = true +} + +func (m MockPeer) JoinServer(s string) error { + panic("should never be called") +} + +func TestContactFunctionality_InValidHostname(t *testing.T) { + cf, _ := FunctionalityGate(map[string]bool{}) + + peer := &MockPeer{ + hasContact: false, + addContact: false, + peerRequest: false, + } + + response := cf.HandleImportString(peer, "") + + if peer.addContact || peer.peerRequest { + t.Fatalf("HandleImportString for a malformed import string should have no resulted in addContact or a peerRequest: %v", peer) + } + + if response.Error() != features.ConstructResponse(addContactPrefix, "invalid_import_string").Error() { + t.Fatalf("Response to a successful import is malformed: %v", response) + } + +} + +func TestContactFunctionality_ValidHostnameExistingContact(t *testing.T) { + cf, _ := FunctionalityGate(map[string]bool{}) + + peer := &MockPeer{ + hasContact: true, + addContact: false, + peerRequest: false, + } + + response := cf.HandleImportString(peer, ValidHostname) + + if peer.addContact || peer.peerRequest { + t.Fatalf("HandleImportString for a valid string should not call addContact or a peerRequest when the contact already exists: %v", peer) + } + + if response.Error() != features.ConstructResponse(addContactPrefix, "contact_already_exists").Error() { + t.Fatalf("Response to a successful import is malformed: %v", response) + } + +} + +func TestContactFunctionality_ValidHostnameUnknownContact(t *testing.T) { + cf, _ := FunctionalityGate(map[string]bool{}) + + peer := &MockPeer{ + hasContact: false, + addContact: false, + peerRequest: false, + } + + response := cf.HandleImportString(peer, ValidHostname) + + if peer.addContact && peer.peerRequest { + if response.Error() != features.ConstructResponse(addContactPrefix, "success").Error() { + t.Fatalf("Response to a successful import is malformed: %v", response) + } + } else { + t.Fatalf("HandleImportString for a valid import string should have resulted in addContact or a peerRequest: %v", peer) + } +} diff --git a/features/response.go b/features/response.go new file mode 100644 index 0000000..dfbf1b5 --- /dev/null +++ b/features/response.go @@ -0,0 +1,13 @@ +package features + +import "errors" + +// Response is a wrapper to better semantically convey the response type... +type Response error + +const errorSeparator = "." + +// ConstructResponse is a helper function for creating Response structures. +func ConstructResponse(prefix string, error string) Response { + return errors.New(prefix + errorSeparator + error) +} diff --git a/go.mod b/go.mod index fcf17f7..ee7bd02 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.openprivacy.ca/flutter/libcwtch-go go 1.15 require ( - cwtch.im/cwtch v0.6.0 + cwtch.im/cwtch v0.6.3 git.openprivacy.ca/openprivacy/connectivity v1.3.3 git.openprivacy.ca/openprivacy/log v1.0.2 -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index 69a00db..8e2203d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,12 @@ cwtch.im/cwtch v0.5.1 h1:84foD/HBebPbA4gUEwp+feakeHkD3Di53Q3FnSbqDMM= cwtch.im/cwtch v0.5.1/go.mod h1:snHZIZwRQPAZG2LRZsN5SpAIbeR597VJoDS+KHm7q9w= cwtch.im/cwtch v0.6.0 h1:LaIRs8dDtnSr/MVFX3giTxnYwSyAIu0w55eZWWO7VZI= cwtch.im/cwtch v0.6.0/go.mod h1:snHZIZwRQPAZG2LRZsN5SpAIbeR597VJoDS+KHm7q9w= +cwtch.im/cwtch v0.6.1 h1:NqfLPS7k3rRi5A0qf/tHoq42BEc/K89LHgZqsBs7Luk= +cwtch.im/cwtch v0.6.1/go.mod h1:snHZIZwRQPAZG2LRZsN5SpAIbeR597VJoDS+KHm7q9w= +cwtch.im/cwtch v0.6.2 h1:UqwVnxNXvhhG7yGpcY9aXyq0dy31XzjV708BWCHHIms= +cwtch.im/cwtch v0.6.2/go.mod h1:snHZIZwRQPAZG2LRZsN5SpAIbeR597VJoDS+KHm7q9w= +cwtch.im/cwtch v0.6.3 h1:AifcbxK60UTeOiOt0ur8PLQeDCuljQLhLqrAOO/8guA= +cwtch.im/cwtch v0.6.3/go.mod h1:snHZIZwRQPAZG2LRZsN5SpAIbeR597VJoDS+KHm7q9w= cwtch.im/tapir v0.2.1 h1:t1YJB9q5sV1A9xwiiwL6WVfw3dwQWLoecunuzT1PQtw= cwtch.im/tapir v0.2.1/go.mod h1:xzzZ28adyUXNkYL1YodcHsAiTt3IJ8Loc29YVn9mIEQ= git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c= diff --git a/lib.go b/lib.go index 70100ca..78c5e72 100644 --- a/lib.go +++ b/lib.go @@ -10,6 +10,7 @@ import ( "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/peer" + contact "git.openprivacy.ca/flutter/libcwtch-go/features/contacts" "encoding/json" "fmt" @@ -73,6 +74,7 @@ func StartCwtch(appDir string, torPath string) { newApp.GetPrimaryBus().Subscribe(event.ACNStatus, acnQueue) newApp.GetPrimaryBus().Subscribe(utils.UpdateGlobalSettings, acnQueue) newApp.GetPrimaryBus().Subscribe(utils.SetLoggingLevel, acnQueue) + newApp.GetPrimaryBus().Subscribe(event.AppError, acnQueue) eventHandler = utils.NewEventHandler(newApp) @@ -168,6 +170,11 @@ func c_SendProfileEvent(onion_ptr *C.char, onion_len C.int, json_ptr *C.char, js SendProfileEvent(onion, eventJson) } +const ( + AddContact = event.Type("AddContact") + ImportString = event.Field("ImportString") +) + // SendProfileEvent is a generic method for Rebroadcasting Profile Events from a UI func SendProfileEvent(onion string, eventJson string) { // Convert the Event Json back to a typed Event Struct, this will make the @@ -185,6 +192,12 @@ func SendProfileEvent(onion string, eventJson string) { // We need to update the local cache // Ideally I think this would be pushed back into Cwtch switch new_event.EventType { + case AddContact: + // Peer Functionality is Always Enabled, so we forgo the existence check... + // TODO: Combine with GroupFunctionality to make a meta-handleimportstring that can do both! + pf, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments) + err := pf.HandleImportString(peer, new_event.Data[ImportString]) + eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: err.Error()})) case event.SetAttribute: peer.SetAttribute(new_event.Data[event.Key], new_event.Data[event.Data]) case event.SetPeerAttribute: diff --git a/utils/eventHandler.go b/utils/eventHandler.go index e3eb31a..05a0a53 100644 --- a/utils/eventHandler.go +++ b/utils/eventHandler.go @@ -290,7 +290,11 @@ func (eh *EventHandler) forwardProfileMessages(onion string, q event.Queue) { // TODO: graceful shutdown, via an injected event of special QUIT type exiting loop/go routine for { e := q.Next() - ev := EventProfileEnvelope{Event: *e, Profile: onion} + ev := EventProfileEnvelope{Event: e, Profile: onion} eh.profileEvents <- ev } } + +func (eh *EventHandler) Push(newEvent event.Event) { + eh.appBusQueue.Publish(newEvent) +} diff --git a/utils/manager.go b/utils/manager.go index 1e788ea..68c2c52 100644 --- a/utils/manager.go +++ b/utils/manager.go @@ -245,6 +245,7 @@ func NewManager(profile string, gcd *GrandCentralDispatcher) Manager { // uiManager.AddContact(onion) // (handle string, displayName string, image string, badge int, status int, authorization string, loading bool, lastMsgTime int) func EnrichNewPeer(handle string, ph *PeerHelper, ev *EventProfileEnvelope) error { + log.Infof("Enriching New Peer %v", handle) if ph.IsGroup(handle) { group := ph.peer.GetGroup(handle) if group != nil { @@ -266,11 +267,19 @@ func EnrichNewPeer(handle string, ph *PeerHelper, ev *EventProfileEnvelope) erro ev.Event.Data["picture"] = ph.GetProfilePic(handle) ev.Event.Data["nick"] = ph.GetNick(handle) - ev.Event.Data["status"] = strconv.Itoa(int(connections.ConnectionStateToType()[contact.State])) + // TODO Replace this if with a better flow that separates New Contacts and Peering Updates + if contact.State == "" { + // Will be disconnected to start + ev.Event.Data["status"] = connections.ConnectionStateName[connections.DISCONNECTED] + } else { + ev.Event.Data["status"] = contact.State + } ev.Event.Data["authorization"] = string(contact.Authorization) ev.Event.Data["loading"] = "false" ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(&contact.Timeline)) + } else { + log.Errorf("Failed to find contact: %v", handle) } } else { // could be a server?