diff --git a/features/contacts/contact_functionality.go b/features/contacts/contact_functionality.go deleted file mode 100644 index ce7d2b7..0000000 --- a/features/contacts/contact_functionality.go +++ /dev/null @@ -1,44 +0,0 @@ -package contact - -import ( - "cwtch.im/cwtch/model" - "cwtch.im/cwtch/peer" - "git.openprivacy.ca/cwtch.im/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 { - err := peer.SendMessage(handle, message) - if err == nil { - return features.ConstructResponse(sendMessagePrefix, "success") - } - return features.ConstructResponse(sendMessagePrefix, err.Error()) -} - -// 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 deleted file mode 100644 index 1771391..0000000 --- a/features/contacts/contact_functionality_addcontact_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package contact - -import ( - "cwtch.im/cwtch/model" - "git.openprivacy.ca/cwtch.im/libcwtch-go/features" - "testing" -) - -const ValidHostname = "openpravyvc6spbd4flzn4g2iqu4sxzsizbtb5aqec25t76dnoo5w7yd" - -type MockPeer struct { - hasContact bool - addContact bool - peerRequest bool -} - -func (m MockPeer) BlockUnknownConnections() { - panic("should never be called") -} - -func (m MockPeer) AllowUnknownConnections() { - panic("should never be called") -} - -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/groups/group_functionality.go b/features/groups/group_functionality.go index 4620ac2..27d1242 100644 --- a/features/groups/group_functionality.go +++ b/features/groups/group_functionality.go @@ -3,12 +3,10 @@ package groups import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/peer" - "encoding/base64" + "cwtch.im/cwtch/protocol/connections" "fmt" - "git.openprivacy.ca/cwtch.im/libcwtch-go/features" - "git.openprivacy.ca/openprivacy/log" - "strings" ) const serverPrefix = "server:" @@ -28,12 +26,6 @@ const ( UpdateServerInfo = event.Type("UpdateServerInfo") ) -// ReadServerInfo is a meta-interface for reading information about servers.. -type ReadServerInfo interface { - peer.ReadContacts - peer.ReadServers -} - // GroupFunctionality provides experiment gated server functionality type GroupFunctionality struct { } @@ -46,27 +38,8 @@ func ExperimentGate(experimentMap map[string]bool) (*GroupFunctionality, error) return nil, fmt.Errorf("gated by %v", groupExperiment) } -// SendMessage is a deprecated api -func (gf *GroupFunctionality) SendMessage(peer peer.CwtchPeer, handle string, message string) (string, error) { - // TODO this auto accepting behaviour needs some thinking through - if !peer.GetGroup(handle).Accepted { - err := peer.AcceptInvite(handle) - if err != nil { - log.Errorf("tried to mark a nonexistent group as existed. bad!") - return "", err - } - } - return "", peer.SendMessage(handle, message) -} - -// ValidPrefix returns true if an import string contains a prefix that indicates it contains information about a -// server or a group -func (gf *GroupFunctionality) ValidPrefix(importString string) bool { - return strings.HasPrefix(importString, tofuBundlePrefix) || strings.HasPrefix(importString, serverPrefix) || strings.HasPrefix(importString, groupPrefix) -} - // GetServerInfoList compiles all the information the UI might need regarding all servers.. -func (gf *GroupFunctionality) GetServerInfoList(profile ReadServerInfo) []Server { +func (gf *GroupFunctionality) GetServerInfoList(profile peer.CwtchPeer) []Server { var servers []Server for _, server := range profile.GetServers() { servers = append(servers, gf.GetServerInfo(server, profile)) @@ -76,52 +49,15 @@ func (gf *GroupFunctionality) GetServerInfoList(profile ReadServerInfo) []Server // GetServerInfo compiles all the information the UI might need regarding a particular server including any verified // cryptographic keys -func (gf *GroupFunctionality) GetServerInfo(serverOnion string, profile peer.ReadContacts) Server { - serverInfo := profile.GetContact(serverOnion) +func (gf *GroupFunctionality) GetServerInfo(serverOnion string, profile peer.CwtchPeer) Server { + serverInfo,_ := profile.FetchConversationInfo(serverOnion) keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass} var serverKeys []ServerKey for _, keyType := range keyTypes { - if key, has := serverInfo.GetAttribute(string(keyType)); has { + if key, has := serverInfo.GetAttribute(attr.PublicScope, attr.ServerKeyZone, string(keyType)); has { serverKeys = append(serverKeys, ServerKey{Type: string(keyType), Key: key}) } } - return Server{Onion: serverOnion, Status: serverInfo.State, Keys: serverKeys} -} - -// HandleImportString handles import strings for groups and servers -func (gf *GroupFunctionality) HandleImportString(peer peer.CwtchPeer, importString string) error { - if strings.HasPrefix(importString, tofuBundlePrefix) { - bundle := strings.Split(importString, "||") - if len(bundle) == 2 { - err := gf.HandleImportString(peer, bundle[0][len(tofuBundlePrefix):]) - // if the server import failed then abort the whole process.. - if err != nil && !strings.HasSuffix(err.Error(), "success") { - return features.ConstructResponse(importBundlePrefix, err.Error()) - } - return gf.HandleImportString(peer, bundle[1]) - } - } else if strings.HasPrefix(importString, serverPrefix) { - // Server Key Bundles are prefixed with - bundle, err := base64.StdEncoding.DecodeString(importString[len(serverPrefix):]) - if err == nil { - if err = peer.AddServer(string(bundle)); err != nil { - return features.ConstructResponse(importBundlePrefix, err.Error()) - } - return features.ConstructResponse(importBundlePrefix, "success") - } - return features.ConstructResponse(importBundlePrefix, err.Error()) - } else if strings.HasPrefix(importString, groupPrefix) { - //eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA== - if gid, err := peer.ImportGroup(importString); err != nil { - return features.ConstructResponse(importBundlePrefix, err.Error()) - } else { - // Auto accept the group here. - if peer.AcceptInvite(gid) != nil { - log.Errorf("Error accepting invite: %v", err) - } - return features.ConstructResponse(importBundlePrefix, "success") - } - } - return features.ConstructResponse(importBundlePrefix, "invalid_group_invite_prefix") -} + return Server{Onion: serverOnion, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys} +} \ No newline at end of file diff --git a/features/servers/servers_functionality.go b/features/servers/servers_functionality.go index 4457218..e07144c 100644 --- a/features/servers/servers_functionality.go +++ b/features/servers/servers_functionality.go @@ -15,8 +15,8 @@ import ( const serversExperiment = "servers-experiment" const ( - ZeroServersLoaded = event.Type("ZeroServersLoaded") - NewServer = event.Type("NewServer") + ZeroServersLoaded = event.Type("ZeroServersLoaded") + NewServer = event.Type("NewServer") ServerIntentUpdate = event.Type("ServerIntentUpdate") ServerDeleted = event.Type("ServerDeleted") ) @@ -31,12 +31,12 @@ const ( ) type ServerInfo struct { - Onion string + Onion string ServerBundle string - Autostart bool - Running bool - Description string - StorageType string + Autostart bool + Running bool + Description string + StorageType string } var lock sync.Mutex @@ -72,7 +72,7 @@ type ServersFunctionality struct { func ExperimentGate(experimentMap map[string]bool) (*ServersFunctionality, error) { if experimentMap[serversExperiment] { lock.Lock() - defer lock.Unlock() + defer lock.Unlock() return &ServersFunctionality{}, nil } return nil, fmt.Errorf("gated by %v", serversExperiment) diff --git a/go.mod b/go.mod index 3a7b799..20714bc 100644 --- a/go.mod +++ b/go.mod @@ -11,3 +11,5 @@ require ( golang.org/x/mod v0.5.0 // indirect golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect ) + +replace cwtch.im/cwtch => /home/sarah/workspace/src/cwtch.im/cwtch \ No newline at end of file diff --git a/go.sum b/go.sum index b742616..4966fb2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,4 @@ -cwtch.im/cwtch v0.12.2 h1:I+ndKadCRCITw4SPbd+1cpRv+z/7iHjjTUv8OzRwTrE= cwtch.im/cwtch v0.12.2/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM= -cwtch.im/cwtch v0.13.0 h1:9WhLG9czfyRceZnHSqfTAq0vfmDC/E20mziJb9+Skrg= -cwtch.im/cwtch v0.13.0/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM= cwtch.im/cwtch v0.13.1 h1:k7CDr16ZLZ+uaRtic2Joooc8TzkO7BkgWXs8r9ilqDY= cwtch.im/cwtch v0.13.1/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM= cwtch.im/cwtch v0.13.2 h1:qbKTQOUryvJpTzIf5iT49x6iAmeNxiz0doNb5phYVEQ= diff --git a/lib.go b/lib.go index 7dfd2e3..c1b39dc 100644 --- a/lib.go +++ b/lib.go @@ -1,6 +1,6 @@ -//package cwtch +package cwtch -package main +//package main // //Needed to invoke C.free // #include @@ -8,6 +8,7 @@ import "C" import ( "crypto/rand" + constants2 "cwtch.im/cwtch/model/constants" "encoding/json" "fmt" "os/user" @@ -23,7 +24,6 @@ import ( "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/peer" "git.openprivacy.ca/cwtch.im/libcwtch-go/constants" - contact "git.openprivacy.ca/cwtch.im/libcwtch-go/features/contacts" "git.openprivacy.ca/cwtch.im/libcwtch-go/features/groups" "git.openprivacy.ca/cwtch.im/libcwtch-go/features/servers" "git.openprivacy.ca/cwtch.im/server" @@ -246,34 +246,25 @@ func ReconnectCwtchForeground() { } settings := utils.ReadGlobalSettings() - + groupHandler,_ := groups.ExperimentGate(settings.Experiments) for _, profileOnion := range peerList { // fix peerpeercontact message counts - contactList := application.GetPeer(profileOnion).GetContacts() - for _, handle := range contactList { - totalMessages := application.GetPeer(profileOnion).GetContact(handle).Timeline.Len() + len(application.GetPeer(profileOnion).GetContact(handle).UnacknowledgedMessages) - eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{ - event.Identity: profileOnion, - event.RemotePeer: handle, - event.Data: strconv.Itoa(totalMessages), - })) - } - - // Group Experiment Refresh - groupHandler, err := groups.ExperimentGate(settings.Experiments) - if err == nil { - // fix peergroupcontact message counts - groupList := application.GetPeer(profileOnion).GetGroups() - for _, groupID := range groupList { - totalMessages := len(application.GetPeer(profileOnion).GetGroup(groupID).GetTimeline()) + profile := application.GetPeer(profileOnion) + conversations,_ := profile.FetchConversations() + for _, conversation := range conversations { + if (conversation.IsGroup() && groupHandler != nil) || conversation.IsServer() == false { + totalMessages,_ := profile.GetChannelMessageCount(conversation.ID, 0) eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{ - event.Identity: profileOnion, - event.GroupID: groupID, - event.Data: strconv.Itoa(totalMessages), + event.Identity: profileOnion, + event.ConversationID: strconv.Itoa(conversation.ID), + event.Data: strconv.Itoa(totalMessages), })) } + } - serverListForOnion := groupHandler.GetServerInfoList(application.GetPeer(profileOnion)) + // Group Experiment Server Refresh + if groupHandler != nil { + serverListForOnion := groupHandler.GetServerInfoList(profile) serversListBytes, _ := json.Marshal(serverListForOnion) eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) } @@ -412,25 +403,6 @@ 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 { - // DEPRECATED: use ImportBundle - case AddContact: - 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()})) - // DEPRECATED: use SetProfileAttribute() - case event.SetAttribute: - log.Errorf("SetAttribute is deprecated.") - // DEPRECATED: use SetContactAttribute() - case event.SetPeerAttribute: - peer.SetContactAttribute(new_event.Data[event.RemotePeer], new_event.Data[event.Key], new_event.Data[event.Data]) - // DEPRECATED: use AcceptContact() and BlockContact() - case event.SetPeerAuthorization: - peer.SetContactAuthorization(new_event.Data[event.RemotePeer], model.Authorization(new_event.Data[event.Authorization])) - - // If approved (e.g. after an unblock) we want to kick off peering again... - if model.Authorization(new_event.Data[event.Authorization]) == model.AuthApproved { - peer.PeerWithOnion(new_event.Data[event.RemotePeer]) - } default: // rebroadcast catch all log.Infof("Received Event %v for %v but no libCwtch handler found, relaying the event directly", new_event, onion) @@ -480,64 +452,26 @@ func LoadProfiles(pass string) { application.LoadProfiles(pass) } -//export c_AcceptContact -func c_AcceptContact(profilePtr *C.char, profileLen C.int, handlePtr *C.char, handleLen C.int) { - AcceptContact(C.GoStringN(profilePtr, profileLen), C.GoStringN(handlePtr, handleLen)) +//export c_c_AcceptConversations +func c_AcceptConversations(profilePtr *C.char, profileLen C.int, conversationID C.int) { + AcceptConversations(C.GoStringN(profilePtr, profileLen), conversationID) } -// AcceptContact takes in a profileOnion and a handle to either a group or a peer and authorizes the handle +// AcceptConversations takes in a profileOnion and a handle to either a group or a peer and authorizes the handle // for further action (e.g. messaging / connecting to the server / joining the group etc.) -func AcceptContact(profileOnion string, handle string) { +func AcceptConversations(profileOnion string, conversationID int) { profile := application.GetPeer(profileOnion) - profileHandler := utils.NewPeerHelper(profile) - if profileHandler.IsGroup(handle) { - profile.AcceptInvite(handle) - } else { - err := profile.SetContactAuthorization(handle, model.AuthApproved) - if err == nil { - eventHandler.Push(event.NewEvent(event.PeerStateChange, map[event.Field]string{ - ProfileOnion: profileOnion, - event.RemotePeer: handle, - "authorization": string(model.AuthApproved), - })) - } else { - log.Errorf("error accepting contact: %s", err.Error()) - } - } -} - -//export c_RejectInvite -func c_RejectInvite(profilePtr *C.char, profileLen C.int, handlePtr *C.char, handleLen C.int) { - RejectInvite(C.GoStringN(profilePtr, profileLen), C.GoStringN(handlePtr, handleLen)) -} - -// RejectInvite rejects a group invite -func RejectInvite(profileOnion string, handle string) { - log.Debugf("rejecting invite %v for %v", handle, profileOnion) - profile := application.GetPeer(profileOnion) - profileHandler := utils.NewPeerHelper(profile) - if profileHandler.IsGroup(handle) { - profile.RejectInvite(handle) - log.Debugf("successfully rejected invite %v for %v", handle, profileOnion) - } + profile.AcceptConversation(conversationID) } //export c_BlockContact -func c_BlockContact(profilePtr *C.char, profileLen C.int, handlePtr *C.char, handleLen C.int) { - BlockContact(C.GoStringN(profilePtr, profileLen), C.GoStringN(handlePtr, handleLen)) +func c_BlockContact(profilePtr *C.char, profileLen C.int, conversationID C.int) { + BlockContact(C.GoStringN(profilePtr, profileLen), conversationID) } -func BlockContact(profile, handle string) { - err := application.GetPeer(profile).SetContactAuthorization(handle, model.AuthBlocked) - if err == nil { - eventHandler.Push(event.NewEvent(event.PeerStateChange, map[event.Field]string{ - ProfileOnion: profile, - event.RemotePeer: handle, - "authorization": string(model.AuthBlocked), - })) - } else { - log.Errorf("error blocking contact: %s", err.Error()) - } +func BlockContact(profileOnion string, conversationID int) { + profile := application.GetPeer(profileOnion) + profile.BlockConversation(conversationID) } //export c_UpdateMessageFlags @@ -655,78 +589,45 @@ func GetMessagesByContentHash(profileOnion, handle string, contentHash string) s } //export c_SendMessage -func c_SendMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, msg_ptr *C.char, msg_len C.int) { +func c_SendMessage(profile_ptr *C.char, profile_len C.int, conversation_id C.int, msg_ptr *C.char, msg_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) - handle := C.GoStringN(handle_ptr, handle_len) msg := C.GoStringN(msg_ptr, msg_len) - SendMessage(profile, handle, msg) + SendMessage(profile, conversation_id, msg) } -func SendMessage(profileOnion, handle, msg string) { +func SendMessage(profileOnion string, conversationID int, msg string) { profile := application.GetPeer(profileOnion) - ph := utils.NewPeerHelper(profile) - if ph.IsGroup(handle) { - groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) - if err == nil { - groupHandler.SendMessage(profile, handle, msg) - profile.SetGroupAttribute(handle, attr.GetLocalScope(constants.Archived), event.False) - } - } else { - contactHandler, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments) - contactHandler.SendMessage(profile, handle, msg) - profile.SetContactAttribute(handle, attr.GetLocalScope(constants.Archived), event.False) - } + profile.SendMessage(conversationID, msg) } //export c_SendInvitation -func c_SendInvitation(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, target_ptr *C.char, target_len C.int) { +func c_SendInvitation(profile_ptr *C.char, profile_len C.int, conversation_id C.int, target_id C.int) { profile := C.GoStringN(profile_ptr, profile_len) - handle := C.GoStringN(handle_ptr, handle_len) - target := C.GoStringN(target_ptr, target_len) - SendInvitation(profile, handle, target) + SendInvitation(profile, conversation_id, target_id) } -// Send an invitation from `profileOnion` to contact `handle` (peer or group) +// SendInvitation sends an invitation from `profileOnion` to contact `handle` (peer or group) // asking them to add the contact `target` (also peer or group). // For groups, the profile must already have `target` as a contact. -func SendInvitation(profileOnion, handle, target string) { +func SendInvitation(profileOnion string, conversationID int, targetID int) { profile := application.GetPeer(profileOnion) - ph := utils.NewPeerHelper(profile) - - var invite ChatMessage - if ph.IsGroup(target) { - bundle, _ := profile.GetContact(profile.GetGroup(target).GroupServer).GetAttribute(string(model.BundleType)) - inviteStr, err := profile.GetGroup(target).Invite() - if err == nil { - invite = ChatMessage{O: 101, D: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), inviteStr)} - } - } else { - invite = ChatMessage{O: 100, D: target} - } - - inviteBytes, err := json.Marshal(invite) - if err != nil { - log.Errorf("malformed invite: %v", err) - } else { - SendMessage(profileOnion, handle, string(inviteBytes)) - } + profile.SendInviteToConversation(conversationID, targetID) } //export c_ShareFile -func c_ShareFile(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, filepath_ptr *C.char, filepath_len C.int) { +func c_ShareFile(profile_ptr *C.char, profile_len C.int, conversationID C.int, filepath_ptr *C.char, filepath_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) - handle := C.GoStringN(handle_ptr, handle_len) sharefilepath := C.GoStringN(filepath_ptr, filepath_len) - ShareFile(profile, handle, sharefilepath) + ShareFile(profile, conversationID, sharefilepath) } -func ShareFile(profileOnion, handle, sharefilepath string) { +func ShareFile(profileOnion string , conversationID int, sharefilepath string) { profile := application.GetPeer(profileOnion) fh, err := filesharing.FunctionalityGate(utils.ReadGlobalSettings().Experiments) if err != nil { log.Errorf("file sharing error: %v", err) } else { - err = fh.ShareFile(sharefilepath, profile, handle) + err = fh.ShareFile(sharefilepath, profile, conversationID) if err != nil { log.Errorf("error sharing file: %v", err) } @@ -734,22 +635,21 @@ func ShareFile(profileOnion, handle, sharefilepath string) { } //export c_DownloadFile -func c_DownloadFile(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, filepath_ptr *C.char, filepath_len C.int, manifestpath_ptr *C.char, manifestpath_len C.int, filekey_ptr *C.char, filekey_len C.int) { +func c_DownloadFile(profile_ptr *C.char, profile_len C.int, conversationID C.int, filepath_ptr *C.char, filepath_len C.int, manifestpath_ptr *C.char, manifestpath_len C.int, filekey_ptr *C.char, filekey_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) - handle := C.GoStringN(handle_ptr, handle_len) downloadfilepath := C.GoStringN(filepath_ptr, filepath_len) manifestpath := C.GoStringN(manifestpath_ptr, manifestpath_len) filekey := C.GoStringN(filekey_ptr, filekey_len) - DownloadFile(profile, handle, downloadfilepath, manifestpath, filekey) + DownloadFile(profile, conversationID, downloadfilepath, manifestpath, filekey) } -func DownloadFile(profileOnion, handle, filepath, manifestpath, filekey string) { +func DownloadFile(profileOnion string, conversationID int, filepath, manifestpath, filekey string) { profile := application.GetPeer(profileOnion) fh, err := filesharing.FunctionalityGate(utils.ReadGlobalSettings().Experiments) if err != nil { log.Errorf("file sharing error: %v", err) } else { - fh.DownloadFile(profile, handle, filepath, manifestpath, filekey) + fh.DownloadFile(profile, conversationID, filepath, manifestpath, filekey) } } @@ -781,19 +681,18 @@ func CheckDownloadStatus(profileOnion, fileKey string) { } //export c_VerifyOrResumeDownload -func c_VerifyOrResumeDownload(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, filekey_ptr *C.char, filekey_len C.int) { +func c_VerifyOrResumeDownload(profile_ptr *C.char, profile_len C.int, conversationID, filekey_ptr *C.char, filekey_len C.int) { profile := C.GoStringN(profile_ptr, profile_len) - handle := C.GoStringN(handle_ptr, handle_len) filekey := C.GoStringN(filekey_ptr, filekey_len) - VerifyOrResumeDownload(profile, handle, filekey) + VerifyOrResumeDownload(profile, conversationID, filekey) } -func VerifyOrResumeDownload(profileOnion, handle, fileKey string) { +func VerifyOrResumeDownload(profileOnion string, conversationID int, fileKey string) { profile := application.GetPeer(profileOnion) if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)); exists { if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists { log.Infof("resuming %s", fileKey) - DownloadFile(profileOnion, handle, downloadfilepath, manifestFilePath, fileKey) + DownloadFile(profileOnion, conversationID, downloadfilepath, manifestFilePath, fileKey) } else { log.Errorf("found manifest path but not download path for %s", fileKey) } @@ -824,11 +723,9 @@ func CreateGroup(profileHandle string, server string, name string) { profile := application.GetPeer(profileHandle) _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) if err == nil { - gid, _, err := profile.StartGroup(server) + conversationID, err := profile.StartGroup(name, server) if err == nil { - log.Debugf("created group %v on %v: $v", profileHandle, server, gid) - // set the group name - profile.SetGroupAttribute(gid, attr.GetLocalScope("name"), name) + log.Debugf("created group %v on %v: $v", profileHandle, server, conversationID) } else { log.Errorf("error creating group or %v on server %v: %v", profileHandle, server, err) } @@ -854,42 +751,27 @@ func DeleteProfile(profile string, password string) { } //export c_ArchiveConversation -func c_ArchiveConversation(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) { +func c_ArchiveConversation(profile_ptr *C.char, profile_len C.int, conversationID C.int) { profile := C.GoStringN(profile_ptr, profile_len) - handle := C.GoStringN(handle_ptr, handle_len) - ArchiveConversation(profile, handle) + ArchiveConversation(profile, conversationID) } // ArchiveConversation sets the conversation to archived -func ArchiveConversation(profileHandle string, handle string) { +func ArchiveConversation(profileHandle string, conversationID int) { profile := application.GetPeer(profileHandle) - ph := utils.NewPeerHelper(profile) - if ph.IsGroup(handle) { - profile.SetGroupAttribute(handle, attr.GetLocalScope(constants.Archived), event.True) - } else { - profile.SetContactAttribute(handle, attr.GetLocalScope(constants.Archived), event.True) - } + profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Archived)), constants2.True) } //export c_DeleteContact -func c_DeleteContact(profile_ptr *C.char, profile_len C.int, hanlde_ptr *C.char, handle_len C.int) { +func c_DeleteContact(profile_ptr *C.char, profile_len C.int, conversationID C.int) { profile := C.GoStringN(profile_ptr, profile_len) - groupID := C.GoStringN(hanlde_ptr, handle_len) - DeleteContact(profile, groupID) + DeleteContact(profile, conversationID) } // DeleteContact removes all trace of the contact from the profile -func DeleteContact(profileHandle string, handle string) { +func DeleteContact(profileHandle string, conversationID int) { profile := application.GetPeer(profileHandle) - ph := utils.NewPeerHelper(profile) - if ph.IsGroup(handle) { - _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) - if err == nil { - profile.DeleteGroup(handle) - } - } else { - profile.DeleteContact(handle) - } + profile.DeleteConversation(conversationID) } //export c_ImportBundle @@ -903,21 +785,16 @@ func c_ImportBundle(profile_ptr *C.char, profile_len C.int, bundle_ptr *C.char, // different formats (e.g. a peer address, a group invite, a server key bundle, or a combination) func ImportBundle(profileOnion string, bundle string) { profile := application.GetPeer(profileOnion) - peerHandler, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments) - response := peerHandler.HandleImportString(profile, bundle) - if strings.Contains(response.Error(), "invalid_import_string") { - groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) - if err == nil { - response = groupHandler.HandleImportString(profile, bundle) - eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: response.Error()})) + response := profile.ImportBundle(bundle) - // We might have added a new server, so refresh the server list... - serverListForOnion := groupHandler.GetServerInfoList(profile) - serversListBytes, _ := json.Marshal(serverListForOnion) - eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) - return - } + // We might have added a new server, so refresh the server list if applicable... + groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) + if err == nil { + serverListForOnion := groupHandler.GetServerInfoList(profile) + serversListBytes, _ := json.Marshal(serverListForOnion) + eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) } + eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: response.Error()})) } @@ -960,7 +837,7 @@ func c_SetContactAttribute(profile_ptr *C.char, profile_len C.int, contact_ptr * // SetContactAttribute provides a wrapper around profile.SetProfileAttribute func SetContactAttribute(profileOnion string, contactHandle string, key string, value string) { profile := application.GetPeer(profileOnion) - profile.SetContactAttribute(contactHandle, key, value) + profile.SetConversationAttribute(contactHandle, key, value) } //export c_SetGroupAttribute @@ -1181,4 +1058,4 @@ func SetServerAttribute(onion string, key string, val string) { // ***** END Server APIs ***** // Leave as is, needed by ffi -func main() {} +//func main() {} diff --git a/quality.sh b/quality.sh index c913c92..81c61d9 100755 --- a/quality.sh +++ b/quality.sh @@ -3,9 +3,6 @@ echo "Checking code quality (you want to see no output here)" echo "" -echo "Vetting:" -go list ./... | xargs go vet - echo "" echo "Linting:" diff --git a/utils/contacts.go b/utils/contacts.go index e9f80c8..4c28052 100644 --- a/utils/contacts.go +++ b/utils/contacts.go @@ -13,4 +13,5 @@ type Contact struct { IsGroup bool `json:"isGroup"` GroupServer string `json:"groupServer"` IsArchived bool `json:"isArchived"` + Identifier int `json:"identifier"` } diff --git a/utils/eventHandler.go b/utils/eventHandler.go index 99921a8..69d22e1 100644 --- a/utils/eventHandler.go +++ b/utils/eventHandler.go @@ -13,6 +13,7 @@ import ( "git.openprivacy.ca/cwtch.im/libcwtch-go/features/servers" "git.openprivacy.ca/openprivacy/log" "strconv" + "time" ) import "cwtch.im/cwtch/event" @@ -61,8 +62,13 @@ func (eh *EventHandler) GetNextEvent() string { select { case e := <-appChan: return eh.handleAppBusEvent(&e) - case ev := <-eh.profileEvents: - return eh.handleProfileEvent(&ev) + default: + select { + case e := <-appChan: + return eh.handleAppBusEvent(&e) + case ev := <-eh.profileEvents: + return eh.handleProfileEvent(&ev) + } } } @@ -117,104 +123,83 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string { } } - picVal, ok := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.Picture) - if !ok { - picVal = ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro)) - } - pic, err := StringToImage(picVal) - if err != nil { - pic = NewImage(RandomProfileImage(onion), TypeImageDistro) - } - picPath := GetPicturePath(pic) - - // Set publicly scopes attributes - profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants2.Picture, picPath) online, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline) - // Name always exists e.Data[constants.Name], _ = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) - e.Data[constants2.Picture] = picPath + e.Data[constants2.Picture] = RandomProfileImage(onion) e.Data["Online"] = online var contacts []Contact var servers []groups.Server - for _, contact := range profile.GetContacts() { + + conversations, err := profile.FetchConversations() + if err != nil { + /// um.... + return "" + } + for _, conversationInfo := range conversations { // Only compile the server info if we have enabled the experiment... // Note that this means that this info can become stale if when first loaded the experiment // has been disabled and then is later re-enabled. As such we need to ensure that this list is // re-fetched when the group experiment is enabled via a dedicated ListServerInfo event... - if profile.GetContact(contact).IsServer() { + if conversationInfo.IsServer() { groupHandler, err := groups.ExperimentGate(ReadGlobalSettings().Experiments) if err == nil { - servers = append(servers, groupHandler.GetServerInfo(contact, profile)) + servers = append(servers, groupHandler.GetServerInfo(conversationInfo.Handle, profile)) } continue } - contactInfo := profile.GetContact(contact) - ph := NewPeerHelper(profile) - name := ph.GetNick(contact) - cpicPath := ph.GetProfilePic(contact) - saveHistory, set := contactInfo.GetAttribute(event.SaveHistoryKey) + name,_ := conversationInfo.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + cpicPath := RandomProfileImage(conversationInfo.Handle) + saveHistory, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, event.SaveHistoryKey) if !set { saveHistory = event.DeleteHistoryDefault } - isArchived, set := contactInfo.GetAttribute(attr.GetLocalScope(constants2.Archived)) + isArchived, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants2.Archived) if !set { isArchived = event.False } - contacts = append(contacts, Contact{ - Name: name, - Onion: contactInfo.Onion, - Status: contactInfo.State, - Picture: cpicPath, - Authorization: string(contactInfo.Authorization), - SaveHistory: saveHistory, - Messages: contactInfo.Timeline.Len(), - Unread: 0, - LastMessage: strconv.Itoa(getLastMessageTime(&contactInfo.Timeline)), - IsGroup: false, - IsArchived: isArchived == event.True, - }) - } - // We compile and send the groups regardless of the experiment flag, and hide them in the UI - for _, groupId := range profile.GetGroups() { - group := profile.GetGroup(groupId) - - // Check that the group is cryptographically valid - if !group.CheckGroup() { - continue + state, set := profile.GetPeerState(conversationInfo.Handle) + if !set { + state = connections.DISCONNECTED } - ph := NewPeerHelper(profile) - cpicPath := ph.GetProfilePic(groupId) - authorization := model.AuthUnknown - if group.Accepted { + if conversationInfo.Accepted { authorization = model.AuthApproved } - isArchived, set := group.GetAttribute(attr.GetLocalScope(constants2.Archived)) - if !set { - isArchived = event.False + + if acl, exists := conversationInfo.ACL[conversationInfo.Handle]; exists && acl.Blocked { + authorization = model.AuthBlocked } - // Use the server state when assessing group state - state := profile.GetContact(group.GroupServer).State + + groupServer,_ := conversationInfo.GetAttribute(attr.LocalScope, attr.LegacyGroupZone, constants.GroupServer) + + count, err := profile.GetChannelMessageCount(conversationInfo.ID, 0) + if err != nil { + log.Errorf("error fetching channel message count %v %v", conversationInfo.ID, err) + } + + lastMessage,_ := profile.GetMostRecentMessages(conversationInfo.ID, 0, 0,1) + contacts = append(contacts, Contact{ - Name: ph.GetNick(groupId), - Onion: group.GroupID, - Status: state, + Name: name, + Identifier: conversationInfo.ID, + Onion: conversationInfo.Handle, + Status: connections.ConnectionStateName[state], Picture: cpicPath, Authorization: string(authorization), - SaveHistory: event.SaveHistoryConfirmed, - Messages: group.Timeline.Len(), + SaveHistory: saveHistory, + Messages: count, Unread: 0, - LastMessage: strconv.Itoa(getLastMessageTime(&group.Timeline)), - IsGroup: true, - GroupServer: group.GroupServer, + LastMessage: strconv.Itoa(getLastMessageTime(lastMessage)), + IsGroup: conversationInfo.IsGroup(), + GroupServer: groupServer, IsArchived: isArchived == event.True, }) } @@ -239,56 +224,59 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string { if eh.app == nil { log.Errorf("eh.app == nil in handleProfileEvent... this shouldnt happen?") } else { - peer := eh.app.GetPeer(ev.Profile) - ph := NewPeerHelper(peer) + profile := eh.app.GetPeer(ev.Profile) log.Debugf("New Profile Event to Handle: %v", ev) switch ev.Event.EventType { - - /* - TODO: still handle this somewhere - network info from plugin Network check - case event.NetworkStatus: - online, _ := peer.GetAttribute(attr.GetLocalScope(constants.PeerOnline)) - if e.Data[event.Status] == plugins.NetworkCheckSuccess && online == event.False { - peer.SetAttribute(attr.GetLocalScope(constants.PeerOnline), event.True) - uiManager.UpdateNetworkStatus(true) - // TODO we may have to reinitialize the peer - } else if e.Data[event.Status] == plugins.NetworkCheckError && online == event.True { - peer.SetAttribute(attr.GetLocalScope(constants.PeerOnline), event.False) - uiManager.UpdateNetworkStatus(false) - }*/ - case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data // only needs contact nickname and picture, for displaying on popup notifications - ev.Event.Data["Nick"] = ph.GetNick(ev.Event.Data["RemotePeer"]) - ev.Event.Data["Picture"] = ph.GetProfilePic(ev.Event.Data["RemotePeer"]) - peer.SetContactAttribute(ev.Event.Data["RemotePeer"], attr.GetLocalScope(constants2.Archived), event.False) + ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"]) + if ci != nil && err == nil { + ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID) + profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False) + } + ev.Event.Data["Nick"],_ = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + ev.Event.Data["Picture"] = RandomProfileImage(ev.Event.Data["RemotePeer"]) + case event.NewMessageFromGroup: // only needs contact nickname and picture, for displaying on popup notifications - ev.Event.Data["Nick"] = ph.GetNick(ev.Event.Data[event.GroupID]) - ev.Event.Data["Picture"] = ph.GetProfilePic(ev.Event.Data[event.GroupID]) - peer.SetGroupAttribute(ev.Event.Data[event.GroupID], attr.GetLocalScope(constants2.Archived), event.False) + ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"]) + if ci != nil && err == nil { + ev.Event.Data["Nick"],_ = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + } + ev.Event.Data["Picture"] = RandomProfileImage(ev.Event.Data[event.GroupID]) + conversationID,_ := strconv.Atoi(ev.Event.Data[event.ConversationID]) + profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False) case event.PeerAcknowledgement: - // No enrichement required + ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"]) + if ci != nil && err == nil { + ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID) + } case event.PeerCreated: handle := ev.Event.Data[event.RemotePeer] - err := EnrichNewPeer(handle, ph, ev) + + ev.Event.Data["unread"] = strconv.Itoa(ph.CountUnread(group.Timeline.GetMessages(), lastRead)) + ev.Event.Data["picture"] = ph.GetProfilePic(handle) + ev.Event.Data["numMessages"] = strconv.Itoa(group.Timeline.Len()) + ev.Event.Data["nick"] = ph.GetNick(handle) + ev.Event.Data["status"] = group.State + ev.Event.Data["authorization"] = string(model.AuthApproved) + ev.Event.Data["loading"] = "false" + ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(&group.Timeline)) + if err != nil { return "" } case event.GroupCreated: // This event should only happen after we have validated the invite, as such the error // condition *should* never happen. - - groupPic := ph.GetProfilePic(ev.Event.Data[event.GroupID]) + groupPic := RandomGroupImage(ev.Event.Data[event.GroupID]) ev.Event.Data["PicturePath"] = groupPic - ev.Event.Data["GroupName"] = ph.GetNick(ev.Event.Data[event.GroupID]) - case event.NewGroup: // This event should only happen after we have validated the invite, as such the error // condition *should* never happen. serializedInvite := ev.Event.Data[event.GroupInvite] if invite, err := model.ValidateInvite(serializedInvite); err == nil { - groupPic := ph.GetProfilePic(invite.GroupID) + groupPic := RandomGroupImage(invite.GroupID) ev.Event.Data["PicturePath"] = groupPic } else { log.Errorf("received a new group event which contained an invalid invite %v. this should never happen and likely means there is a bug in cwtch. Please file a ticket @ https://git.openprivcy.ca/cwtch.im/cwtch", err) @@ -296,10 +284,10 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string { } case event.PeerStateChange: cxnState := connections.ConnectionStateToType()[ev.Event.Data[event.ConnectionState]] - contact := peer.GetContact(ev.Event.Data[event.RemotePeer]) + contact,_ := profile.FetchConversationInfo(ev.Event.Data[event.RemotePeer]) if cxnState == connections.AUTHENTICATED && contact == nil { - peer.AddContact(ev.Event.Data[event.RemotePeer], ev.Event.Data[event.RemotePeer], model.AuthUnknown) + profile.NewContactConversation(ev.Event.Data[event.RemotePeer], model.AccessControl{Read: false, Append: false, Blocked: false}, false) return "" } @@ -308,21 +296,21 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string { //uiManager.UpdateContactStatus(contact.Onion, int(cxnState), false) if cxnState == connections.AUTHENTICATED { // if known and authed, get vars - peer.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants.Name) - peer.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants2.Picture) + profile.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants.Name) + profile.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants2.Picture) } } case event.NewRetValMessageFromPeer: // auto handled event means the setting is already done, we're just deciding if we need to tell the UI - onion := ev.Event.Data[event.RemotePeer] + conversationID,_ := strconv.Atoi(ev.Event.Data[event.ConversationID]) scope := ev.Event.Data[event.Scope] path := ev.Event.Data[event.Path] - //val := ev.Event.Data[event.Data] exists, _ := strconv.ParseBool(ev.Event.Data[event.Exists]) if exists && attr.IntoScope(scope) == attr.PublicScope { - if _, exists := peer.GetContactAttribute(onion, attr.GetLocalScope(path)); exists { + zone,path := attr.ParseZone(path) + if _, err := profile.GetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(path))); err != nil { // we have a locally set override, don't pass this remote set public scope update to UI return "" } @@ -361,7 +349,6 @@ func (eh *EventHandler) startHandlingPeer(onion string) { eventBus.Subscribe(event.SendMessageToPeerError, q) eventBus.Subscribe(event.ServerStateChange, q) eventBus.Subscribe(event.PeerStateChange, q) - eventBus.Subscribe(event.PeerCreated, q) eventBus.Subscribe(event.NetworkStatus, q) eventBus.Subscribe(event.ChangePasswordSuccess, q) eventBus.Subscribe(event.ChangePasswordError, q) @@ -395,3 +382,14 @@ func (eh *EventHandler) forwardProfileMessages(onion string, q event.Queue) { func (eh *EventHandler) Push(newEvent event.Event) { eh.appBusQueue.Publish(newEvent) } + +func getLastMessageTime(conversationMessages []model.ConversationMessage) int { + if len(conversationMessages) == 0 { + return 0 + } + time, err := time.Parse(time.RFC3339Nano, conversationMessages[0].Attr[constants.AttrSentTimestamp]) + if err != nil { + return 0 + } + return int(time.Unix()) +} \ No newline at end of file diff --git a/utils/manager.go b/utils/manager.go deleted file mode 100644 index 056c23a..0000000 --- a/utils/manager.go +++ /dev/null @@ -1,251 +0,0 @@ -package utils - -import ( - "cwtch.im/cwtch/model" - "cwtch.im/cwtch/model/attr" - "cwtch.im/cwtch/peer" - "cwtch.im/cwtch/protocol/connections" - "errors" - "git.openprivacy.ca/cwtch.im/libcwtch-go/constants" - "git.openprivacy.ca/openprivacy/log" - "strconv" - "strings" - "time" -) - -type PeerHelper struct { - peer peer.CwtchPeer -} - -func NewPeerHelper(profile peer.CwtchPeer) *PeerHelper { - return &PeerHelper{profile} -} - -func (p *PeerHelper) IsGroup(id string) bool { - return len(id) == 32 && !p.IsServer(id) -} - -func (p *PeerHelper) IsPeer(id string) bool { - return len(id) == 56 && !p.IsServer(id) -} - -// Check if the id is associated with a contact with a KeyTypeServerOnion attribute (which indicates that this -// is a server, not a regular contact or a group -func (p *PeerHelper) IsServer(id string) bool { - _, ok := p.peer.GetContactAttribute(id, string(model.KeyTypeServerOnion)) - return ok -} - -// GetTimeline returns a pointer to the timeline associated with the conversation handle or nil if the handle -// does not exist (this can happen if the conversation has been deleted) -func (p *PeerHelper) GetTimeline(handle string) *model.Timeline { - if p.IsServer(handle) { - // This should *never* happen - log.Errorf("server accessed as contact when getting timeline...") - return &model.Timeline{} - } - // We return a pointer to the timeline to avoid copying, accessing Timeline is thread-safe - if p.IsGroup(handle) { - group := p.peer.GetGroup(handle) - if group == nil { - return nil - } - return &group.Timeline - } - contact := p.peer.GetContact(handle) - if contact == nil { - return nil - } - return &contact.Timeline -} - -/* -func getOrDefault(id, key string, defaultVal string) string { - var val string - var ok bool - if IsGroup(id) { - val, ok = the.Peer.GetGroupAttribute(id, key) - } else { - val, ok = the.Peer.GetContactAttribute(id, key) - } - if ok { - return val - } else { - return defaultVal - } -}*/ - -func (p *PeerHelper) GetWithSetDefault(id string, key string, defaultVal string) string { - var val string - var ok bool - if p.IsGroup(id) { - val, ok = p.peer.GetGroupAttribute(id, key) - } else { - val, ok = p.peer.GetContactAttribute(id, key) - } - if !ok { - val = defaultVal - if p.IsGroup(id) { - p.peer.SetGroupAttribute(id, key, defaultVal) - } else { - p.peer.SetContactAttribute(id, key, defaultVal) - } - } - return val -} - -func (p *PeerHelper) GetNick(id string) string { - if p.IsGroup(id) { - nick, exists := p.peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Name)) - if !exists || nick == "" || nick == id { - nick, exists = p.peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Name)) - if !exists { - nick = "[" + id + "]" - } - } - return nick - } else { - nick, exists := p.peer.GetContactAttribute(id, attr.GetLocalScope(constants.Name)) - if !exists || nick == "" || nick == id { - nick, exists = p.peer.GetContactAttribute(id, attr.GetPeerScope(constants.Name)) - if !exists { - nick = "[" + id + "]" - // we do not have a canonical nick for this contact. - // re-request if authenticated - // TODO: This check probably doesn't belong here... - if contact := p.peer.GetContact(id); contact != nil && contact.State == connections.ConnectionStateName[connections.AUTHENTICATED] { - p.peer.SendScopedZonedGetValToContact(id, attr.PublicScope, attr.ProfileZone, constants.Name) - } - } - } - return nick - } -} - -// InitLastReadTime checks and gets the Attributable's LastRead time or sets it to now -func (p *PeerHelper) InitLastReadTime(id string) time.Time { - nowStr, _ := time.Now().MarshalText() - lastReadAttr := p.GetWithSetDefault(id, attr.GetLocalScope(constants.LastRead), string(nowStr)) - var lastRead time.Time - lastRead.UnmarshalText([]byte(lastReadAttr)) - return lastRead -} - -// GetProfilePic returns a string path to an image to display for hte given peer/group id -func (p *PeerHelper) GetProfilePic(id string) string { - if p.IsGroup(id) { - if picVal, exists := p.peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Picture)); exists { - pic, err := StringToImage(picVal) - if err == nil { - return GetPicturePath(pic) - } - } - if picVal, exists := p.peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Picture)); exists { - pic, err := StringToImage(picVal) - if err == nil { - return GetPicturePath(pic) - } - } - return GetPicturePath(NewImage(RandomGroupImage(id), TypeImageDistro)) - - } else { - if picVal, exists := p.peer.GetContactAttribute(id, attr.GetLocalScope(constants.Picture)); exists { - pic, err := StringToImage(picVal) - if err == nil { - return GetPicturePath(pic) - } - } - if picVal, exists := p.peer.GetContactAttribute(id, attr.GetPeerScope(constants.Picture)); exists { - pic, err := StringToImage(picVal) - if err == nil { - return GetPicturePath(pic) - } - } - return RandomProfileImage(id) - } -} - -// a lot of pics were stored full path + uri. remove all this to the relative path in images/ -// fix for storing full paths introduced 2019.12 -func profilePicRelativize(filename string) string { - parts := strings.Split(filename, "qml/images") - return parts[len(parts)-1] -} - -func GetPicturePath(pic *image) string { - switch pic.T { - case TypeImageDistro: - return profilePicRelativize(pic.Val) - default: - log.Errorf("Unhandled profile picture type of %v\n", pic.T) - return "" - } -} - -func (p *PeerHelper) CountUnread(messages []model.Message, lastRead time.Time) int { - count := 0 - for i := len(messages) - 1; i >= 0; i-- { - if messages[i].Timestamp.After(lastRead) || messages[i].Timestamp.Equal(lastRead) { - count++ - } else { - break - } - } - return count -} - -func getLastMessageTime(tl *model.Timeline) int { - if len(tl.Messages) == 0 { - return 0 - } - - return int(tl.Messages[len(tl.Messages)-1].Timestamp.Unix()) -} - -// EnrichNewPeer populates required data for use by frontend -// 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 { - if ph.IsGroup(handle) { - group := ph.peer.GetGroup(handle) - if group != nil { - lastRead := ph.InitLastReadTime(group.GroupID) - ev.Event.Data["unread"] = strconv.Itoa(ph.CountUnread(group.Timeline.GetMessages(), lastRead)) - ev.Event.Data["picture"] = ph.GetProfilePic(handle) - ev.Event.Data["numMessages"] = strconv.Itoa(group.Timeline.Len()) - ev.Event.Data["nick"] = ph.GetNick(handle) - ev.Event.Data["status"] = group.State - ev.Event.Data["authorization"] = string(model.AuthApproved) - ev.Event.Data["loading"] = "false" - ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(&group.Timeline)) - } - } else if ph.IsPeer(handle) { - contact := ph.peer.GetContact(handle) - if contact != nil { - lastRead := ph.InitLastReadTime(contact.Onion) - ev.Event.Data["unread"] = strconv.Itoa(ph.CountUnread(contact.Timeline.GetMessages(), lastRead)) - ev.Event.Data["numMessages"] = strconv.Itoa(contact.Timeline.Len()) - ev.Event.Data["picture"] = ph.GetProfilePic(handle) - - ev.Event.Data["nick"] = ph.GetNick(handle) - - // 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? - log.Debugf("sorry, unable to handle AddContact(%v)", handle) - return errors.New("not a peer or group") - } - return nil -} \ No newline at end of file