From f9b4e1179e09ec3d86d68343307b1421af4585fe Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 15 Apr 2021 15:17:50 -0700 Subject: [PATCH] Group Functionality Experiments with Server Lists --- features/groups/group_functionality.go | 64 +++++++++++++++++---- features/groups/group_functionality_test.go | 6 +- features/groups/server.go | 12 ++++ go.mod | 3 +- go.sum | 13 +++++ lib.go | 48 +++++++++++++--- utils/eventHandler.go | 32 ++++++++--- utils/settings.go | 28 ++++----- 8 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 features/groups/server.go diff --git a/features/groups/group_functionality.go b/features/groups/group_functionality.go index 8ce5f7a..618b51d 100644 --- a/features/groups/group_functionality.go +++ b/features/groups/group_functionality.go @@ -1,6 +1,8 @@ package groups import ( + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model" "cwtch.im/cwtch/peer" "encoding/base64" "errors" @@ -9,22 +11,40 @@ import ( "strings" ) -const ServerPrefix = "server:" -const TofuBundlePrefix = "tofubundle:" -const GroupPrefix = "torv3" -const GroupExperiment = "tapir-groups-experiment" +const serverPrefix = "server:" +const tofuBundlePrefix = "tofubundle:" +const groupPrefix = "torv3" +const groupExperiment = "tapir-groups-experiment" +const ( + // ServerList is a json encoded list of servers + ServerList = event.Field("ServerList") +) + +const ( + // UpdateServerInfo is an event containing a ProfileOnion and a ServerList + 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 { } // ExperimentGate returns GroupFunctionality if the experiment is enabled, and an error otherwise. func ExperimentGate(experimentMap map[string]bool) (*GroupFunctionality, error) { - if experimentMap[GroupExperiment] { + if experimentMap[groupExperiment] { return new(GroupFunctionality), nil } - return nil, fmt.Errorf("gated by %v", GroupExperiment) + return nil, fmt.Errorf("gated by %v", groupExperiment) } +// SendMessage is a deprecated api func (gf *GroupFunctionality) SendMessage(peer peer.CwtchPeer, handle string, message string) error { // TODO this auto accepting behaviour needs some thinking through if !peer.GetGroup(handle).Accepted { @@ -43,25 +63,49 @@ func (gf *GroupFunctionality) SendMessage(peer peer.CwtchPeer, handle string, me // 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) + 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 { + var servers []Server + for _, server := range profile.GetServers() { + servers = append(servers, gf.GetServerInfo(server, profile)) + } + return servers +} + +// 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) + keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass} + var serverKeys []ServerKey + + for _, keyType := range keyTypes { + if key, has := serverInfo.GetAttribute(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) { + if strings.HasPrefix(importString, tofuBundlePrefix) { bundle := strings.Split(importString, "||") gf.HandleImportString(peer, bundle[0][11:]) gf.HandleImportString(peer, bundle[1]) return nil - } else if strings.HasPrefix(importString, ServerPrefix) { + } else if strings.HasPrefix(importString, serverPrefix) { // Server Key Bundles are prefixed with bundle, err := base64.StdEncoding.DecodeString(importString[7:]) if err == nil { return peer.AddServer(string(bundle)) } return err - } else if strings.HasPrefix(importString, GroupPrefix) { + } else if strings.HasPrefix(importString, groupPrefix) { //eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA== return peer.ImportGroup(importString) } diff --git a/features/groups/group_functionality_test.go b/features/groups/group_functionality_test.go index 4a3331a..8d9c871 100644 --- a/features/groups/group_functionality_test.go +++ b/features/groups/group_functionality_test.go @@ -3,7 +3,7 @@ package groups import "testing" func TestGroupFunctionality_ValidPrefix(t *testing.T) { - gf, _ := ExperimentGate(map[string]bool{GroupExperiment: true}) + gf, _ := ExperimentGate(map[string]bool{groupExperiment: true}) if gf.ValidPrefix("torv3blahblahblah") == false { t.Fatalf("torv3 should be a valid prefix") } @@ -26,13 +26,13 @@ func TestGroupFunctionality_IsEnabled(t *testing.T) { t.Fatalf("group functionality should be disabled") } - _, err = ExperimentGate(map[string]bool{GroupExperiment: true}) + _, err = ExperimentGate(map[string]bool{groupExperiment: true}) if err != nil { t.Fatalf("group functionality should be enabled") } - _, err = ExperimentGate(map[string]bool{GroupExperiment: false}) + _, err = ExperimentGate(map[string]bool{groupExperiment: false}) if err == nil { t.Fatalf("group functionality should be disabled") } diff --git a/features/groups/server.go b/features/groups/server.go new file mode 100644 index 0000000..8ac07f7 --- /dev/null +++ b/features/groups/server.go @@ -0,0 +1,12 @@ +package groups + +type ServerKey struct { + Type string `json:"type"` + Key string `json:"key"` +} + +type Server struct { + Onion string `json:"onion"` + Status string `json:"status"` + Keys []ServerKey `json:"keys"` +} diff --git a/go.mod b/go.mod index 441eb16..bcbe7ba 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( cwtch.im/cwtch v0.6.6 git.openprivacy.ca/openprivacy/connectivity v1.4.2 git.openprivacy.ca/openprivacy/log v1.0.2 -) \ No newline at end of file + golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect +) diff --git a/go.sum b/go.sum index 419c83d..5d9e512 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ git.openprivacy.ca/openprivacy/log v1.0.0/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQN git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw= git.openprivacy.ca/openprivacy/log v1.0.2 h1:HLP4wsw4ljczFAelYnbObIs821z+jgMPCe8uODPnGQM= git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -80,14 +81,23 @@ go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -119,8 +129,11 @@ golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200625195345-7480c7b4547d h1:V1BGE5ZHrUIYZYNEm0i7jrPwSo3ks0HSn1TrartSqME= golang.org/x/tools v0.0.0-20200625195345-7480c7b4547d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/lib.go b/lib.go index 775cbb1..4e40cdb 100644 --- a/lib.go +++ b/lib.go @@ -11,6 +11,7 @@ import ( "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/peer" contact "git.openprivacy.ca/flutter/libcwtch-go/features/contacts" + "git.openprivacy.ca/flutter/libcwtch-go/features/groups" "git.openprivacy.ca/openprivacy/connectivity" "encoding/json" @@ -28,6 +29,10 @@ import ( "time" ) +const ( + profileOnion = event.Field("profileOnion") +) + var application app.Application var eventHandler *utils.EventHandler var acnQueue event.Queue @@ -97,12 +102,9 @@ func StartCwtch(appDir string, torPath string) { settings := utils.ReadGlobalSettings() settingsJson, _ := json.Marshal(settings) - newApp.LoadProfiles("be gay do crime") application = newApp - - // Send global settings to the UI... application.GetPrimaryBus().Publish(event.NewEvent(utils.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) log.Infof("libcwtch-go application launched") @@ -149,6 +151,16 @@ func SendAppEvent(eventJson string) { log.Debugf("New Settings %v", globalSettings) utils.WriteGlobalSettings(globalSettings) + // Group Experiment Refresh + groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) + if err == nil { + for profileOnion := range application.ListPeers() { + serverListForOnion := groupHandler.GetServerInfoList(application.GetPeer(profileOnion)) + serversListBytes, _ := json.Marshal(serverListForOnion) + eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)})) + } + } + // Explicitly toggle blocking/unblocking of unknown connections for profiles // that have been loaded. if utils.ReadGlobalSettings().BlockUnknownConnections { @@ -349,7 +361,7 @@ func AcceptContact(profile, handle string) { err := application.GetPeer(profile).SetContactAuthorization(handle, model.AuthApproved) if err == nil { eventHandler.Push(event.NewEvent(event.PeerStateChange, map[event.Field]string{ - "ProfileOnion": profile, + profileOnion: profile, event.RemotePeer: handle, "authorization": string(model.AuthApproved), })) @@ -367,7 +379,7 @@ 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, + profileOnion: profile, event.RemotePeer: handle, "authorization": string(model.AuthBlocked), })) @@ -385,9 +397,9 @@ func DebugResetContact(profile, handle string) { err := application.GetPeer(profile).SetContactAuthorization(handle, model.AuthUnknown) if err == nil { application.GetPrimaryBus().Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{ - "ProfileOnion": profile, - event.RemotePeer: handle, - "authorization": string(model.AuthUnknown), + profileOnion: profile, + event.RemotePeer: handle, + "authorization": string(model.AuthUnknown), })) } else { log.Errorf("error resetting contact: %s", err.Error()) @@ -454,5 +466,25 @@ func ResetTor() { globalACN.Restart() } +//export c_CreateGroup +func c_CreateGroup(profile_ptr *C.char, profile_len C.int, server_ptr *C.char, server_len C.int) { + profile := C.GoStringN(profile_ptr, profile_len) + server := C.GoStringN(server_ptr, server_len) + CreateGroup(profile, server) +} + +func CreateGroup(profile string, server string) { + peer := application.GetPeer(profile) + _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) + if err == nil { + gid, _, err := peer.StartGroup(server) + if err == nil { + log.Debugf("created group %v on %v: $v", profile, server, gid) + } else { + log.Errorf("error creating group or %v on server %v: %v", profile, server, err) + } + } +} + // Leave as is, needed by ffi func main() {} diff --git a/utils/eventHandler.go b/utils/eventHandler.go index c5bd492..7d55577 100644 --- a/utils/eventHandler.go +++ b/utils/eventHandler.go @@ -118,9 +118,18 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string { e.Data["Online"] = online var contacts []Contact + var servers []groups.Server for _, contact := range profile.GetContacts() { + + // 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() { - continue + groupHandler, err := groups.ExperimentGate(ReadGlobalSettings().Experiments) + if err == nil { + servers = append(servers, groupHandler.GetServerInfo(contact, profile)) + } } contactInfo := profile.GetContact(contact) @@ -132,20 +141,25 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string { saveHistory = event.DeleteHistoryDefault } contacts = append(contacts, Contact{ - Name: name, - Onion: contactInfo.Onion, - Status: contactInfo.State, - Picture: cpicPath, + 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)), + SaveHistory: saveHistory, + Messages: contactInfo.Timeline.Len(), + Unread: 0, + LastMessage: strconv.Itoa(getLastMessageTime(&contactInfo.Timeline)), }) } bytes, _ := json.Marshal(contacts) e.Data["ContactsJson"] = string(bytes) + + // Marshal the server list into the new peer event... + serversListBytes, _ := json.Marshal(servers) + e.Data[groups.ServerList] = string(serversListBytes) + log.Infof("contactsJson %v", e.Data["ContactsJson"]) } diff --git a/utils/settings.go b/utils/settings.go index f60e4d8..05b4c94 100644 --- a/utils/settings.go +++ b/utils/settings.go @@ -21,24 +21,24 @@ const GlobalSettingsFilename = "ui.globals" const saltFile = "SALT" type GlobalSettings struct { - Locale string - Theme string - PreviousPid int64 - ExperimentsEnabled bool - Experiments map[string]bool + Locale string + Theme string + PreviousPid int64 + ExperimentsEnabled bool + Experiments map[string]bool BlockUnknownConnections bool - StateRootPane int - FirstTime bool + StateRootPane int + FirstTime bool } var DefaultGlobalSettings = GlobalSettings{ - Locale: "en", - Theme: "light", - PreviousPid: -1, - ExperimentsEnabled: false, - Experiments: make(map[string]bool), - StateRootPane: 0, - FirstTime: true, + Locale: "en", + Theme: "light", + PreviousPid: -1, + ExperimentsEnabled: false, + Experiments: make(map[string]bool), + StateRootPane: 0, + FirstTime: true, BlockUnknownConnections: false, }