diff --git a/go.mod b/go.mod index e71241b9..cfec9878 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module cwtch.im/ui go 1.12 require ( - cwtch.im/cwtch v0.3.16 - git.openprivacy.ca/openprivacy/connectivity v1.1.4 + cwtch.im/cwtch v0.4.0 + git.openprivacy.ca/openprivacy/connectivity v1.2.1 git.openprivacy.ca/openprivacy/log v1.0.1 github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect golang.org/x/crypto v0.0.0-20200420104511-884d27f42877 // indirect -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index 127c46e7..103004b3 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,16 @@ cwtch.im/cwtch v0.3.15 h1:Z7fFREwXY728q2YmmwgHL357zAobrsWJ2oPkkGwzvo0= cwtch.im/cwtch v0.3.15/go.mod h1:iI9q4C3njHFBYQkNEbzMdK6QWPS0Vbkc0FigRHZNTvM= cwtch.im/cwtch v0.3.16 h1:4M5So2zRDjy5byzd3G8ZrA2ZWObfm/oSIRfMBIFdOuI= cwtch.im/cwtch v0.3.16/go.mod h1:iI9q4C3njHFBYQkNEbzMdK6QWPS0Vbkc0FigRHZNTvM= +cwtch.im/cwtch v0.4.0 h1:lhGQiYRBqSF0Pif9QttYVL4B1Oy1vc0v3cZejL7c7x4= +cwtch.im/cwtch v0.4.0/go.mod h1:EvZQDbvXNu38m785dWF0MMljqJzwWrNTFT40HvoEAhI= cwtch.im/tapir v0.1.15 h1:XSCWOvjmNkzMT2IceFgTBXWGKtYfr3a8o+La1s10OhE= cwtch.im/tapir v0.1.15/go.mod h1:HzezugpEx+nZ3LdyDsl0w6n45IJYnOt8uqldkLWmaqs= cwtch.im/tapir v0.1.17 h1:2jVZUe1a88tMI4aJPvRTO4Id3NN3PsM62cT5lntEChk= cwtch.im/tapir v0.1.17/go.mod h1:HzezugpEx+nZ3LdyDsl0w6n45IJYnOt8uqldkLWmaqs= cwtch.im/tapir v0.1.18 h1:Fs/jL9ZRyel/A1D/BYzIPEVQau8y5BJg44yA+GQDbSM= cwtch.im/tapir v0.1.18/go.mod h1:/IrAI6CBHfgzsfgRT8WHVb1P9fCCz7+45hfsdkKn8Zg= +cwtch.im/tapir v0.2.0 h1:7MkoR5+uEuPW34/O0GZRidnIjq/01Cfm8nl5IRuqpGc= +cwtch.im/tapir v0.2.0/go.mod h1:xzzZ28adyUXNkYL1YodcHsAiTt3IJ8Loc29YVn9mIEQ= git.openprivacy.ca/openprivacy/connectivity v1.1.0/go.mod h1:4P8mirZZslKbo2zBrXXVjgEdqGwHo/6qoFBwFQW6d6E= git.openprivacy.ca/openprivacy/connectivity v1.1.1 h1:hKxBOmxP7Jdu3K1BJ93mRtKNiWUoP6YHt/o2snE2Z0w= git.openprivacy.ca/openprivacy/connectivity v1.1.1/go.mod h1:4P8mirZZslKbo2zBrXXVjgEdqGwHo/6qoFBwFQW6d6E= @@ -25,6 +29,8 @@ git.openprivacy.ca/openprivacy/connectivity v1.1.4 h1:/I9epvNNjM8rR/q5y9Y63D9/aP git.openprivacy.ca/openprivacy/connectivity v1.1.4/go.mod h1:4P8mirZZslKbo2zBrXXVjgEdqGwHo/6qoFBwFQW6d6E= git.openprivacy.ca/openprivacy/connectivity v1.2.0 h1:dbZ5CRl11vg3BNHdzRKSlDP8OUtDB+mf6FkxMVf73qw= git.openprivacy.ca/openprivacy/connectivity v1.2.0/go.mod h1:B7vzuVmChJtSKoh0ezph5vu6DQ0gIk0zHUNG6IgXCcA= +git.openprivacy.ca/openprivacy/connectivity v1.2.1 h1:oRL56TR9ZQnKkGkTIQ9wYbJ2IkOOsi/zLYExYiAS+sE= +git.openprivacy.ca/openprivacy/connectivity v1.2.1/go.mod h1:B7vzuVmChJtSKoh0ezph5vu6DQ0gIk0zHUNG6IgXCcA= git.openprivacy.ca/openprivacy/libricochet-go v1.0.11 h1:C7QFFzG0p5XKu0zcOIdLGwEpA9uU0BceBM7CfVK5D40= git.openprivacy.ca/openprivacy/libricochet-go v1.0.11/go.mod h1:yTMps/ZpYS+BNBBvANsNAft28FXrBvFHQauMYNWPrwE= git.openprivacy.ca/openprivacy/libricochet-go v1.0.13 h1:Z86uL9K47onznY1wP1P/wWfWMbbyvk6xnCp94R180os= diff --git a/go/handlers/appHandler.go b/go/handlers/appHandler.go index 851f0fc3..43414c91 100644 --- a/go/handlers/appHandler.go +++ b/go/handlers/appHandler.go @@ -87,7 +87,6 @@ func App(gcd *ui.GrandCentralDispatcher, subscribed chan bool, reloadingAccounts if e.Data[event.Status] != "running" { p.Listen() p.StartPeersConnections() - p.StartGroupConnections() } blockUnkownPeers, exists := p.GetAttribute(constants.BlockUnknownPeersSetting) diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go index c4f940db..30637f95 100644 --- a/go/handlers/peerHandler.go +++ b/go/handlers/peerHandler.go @@ -127,7 +127,7 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { if state == connections.AUTHENTICATED { loading = true } - uiManager.UpdateContactStatus(group.GroupID, int(state), loading) + uiManager.UpdateContactStatus(serverOnion, int(state), loading) } else { log.Errorf("found group that is nil :/") } diff --git a/go/ui/gcd.go b/go/ui/gcd.go index 3a855d0a..41f327b0 100644 --- a/go/ui/gcd.go +++ b/go/ui/gcd.go @@ -1,6 +1,7 @@ package ui import ( + "encoding/base64" "sync" "cwtch.im/cwtch/app" @@ -82,6 +83,7 @@ type GrandCentralDispatcher struct { _ func(locale string, zoom float32, theme string) `signal:"SupplySettings"` _ func(groupID, name, server, invitation string, accepted bool, addrbooknames, addrbookaddrs []string) `signal:"SupplyGroupSettings"` _ func(onion, nick string, authorization string, storage string) `signal:"SupplyPeerSettings"` + _ func(server string, key_types []string, keys []string) `signal:"SupplyServerSettings"` // signals emitted from the ui (written in go, below) // ui @@ -118,6 +120,8 @@ type GrandCentralDispatcher struct { _ func(onion string) `signal:"storeHistoryForPeer,auto"` _ func(onion string) `signal:"deleteHistoryForPeer,auto"` + _ func(handle string) `signal:"requestServerSettings,auto"` + _ func() `constructor:"init"` } @@ -380,6 +384,31 @@ func (this *GrandCentralDispatcher) deleteHistoryForPeer(onion string) { the.Peer.SetContactAttribute(onion, event.SaveHistoryKey, event.DeleteHistoryConfirmed) } +func (this *GrandCentralDispatcher) requestServerSettings(groupID string) { + group := the.Peer.GetGroup(groupID) + + if group == nil { + log.Errorf("couldn't find group %v", groupID) + return + } + + serverInfo := the.Peer.GetContact(group.GroupServer) + + key_types := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass} + var keyNames []string + var keys []string + + for _, key_type := range key_types { + log.Debugf("Looking up %v %v", key_type, keyNames) + if key, has := serverInfo.GetAttribute(string(key_type)); has { + keyNames = append(keyNames, string(key_type)) + keys = append(keys, key) + } + } + + this.SupplyServerSettings(group.GroupServer, keyNames, keys) +} + func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { group := the.Peer.GetGroup(groupID) @@ -397,6 +426,9 @@ func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { contactnames[i] = getNick(contact) } this.SupplyGroupSettings(group.GroupID, nick, group.GroupServer, invite, group.Accepted, contactnames, contactaddrs) + status := connections.ConnectionStateToType[group.State] + log.Debugf("Sending New Group Status: %v %v", group.GroupServer, status) + this.UpdateContactStatus(group.GroupServer, int(status), false) } func (this *GrandCentralDispatcher) saveGroupSettings(groupID, nick string) { @@ -429,6 +461,29 @@ func (this *GrandCentralDispatcher) importString(str string) { return } + if strings.HasPrefix(str, "tofubundle:") { + bundle := strings.Split(str,"||") + this.importString(bundle[0][11:]) + this.importString(bundle[1]) + return + } + + // Server Key Bundles are prefixed with + if strings.HasPrefix(str, "server:") { + bundle, err := base64.StdEncoding.DecodeString(str[7:]) + if err == nil { + err := the.Peer.AddServer(string(bundle)) + if err == nil { + this.InvokePopup("Successfully Imported Server Key Bundle") + return + } + this.InvokePopup("Error Importing Server Key Bundle: " + err.Error()) + return + } + this.InvokePopup("Invalid Server Key Bundle: " + err.Error()) + return + } + log.Debugf("importing: %s\n", str) onion := str name := onion @@ -441,6 +496,7 @@ func (this *GrandCentralDispatcher) importString(str string) { this.InvokePopup("not a valid group invite") return } + return } @@ -650,7 +706,9 @@ func (this *GrandCentralDispatcher) loadProfile(onion string) { contacts := the.Peer.GetContacts() for i := range contacts { - this.GetUiManager(this.selectedProfile()).AddContact(contacts[i]) + if the.Peer.GetContact(contacts[i]).IsServer() == false { + this.GetUiManager(this.selectedProfile()).AddContact(contacts[i]) + } } groups := the.Peer.GetGroups() @@ -689,7 +747,7 @@ func (this *GrandCentralDispatcher) storeSetting(key, val string) { func (this *GrandCentralDispatcher) reloadProfileList() { this.ResetProfileList() - for onion, _ := range the.CwtchApp.ListPeers() { + for onion := range the.CwtchApp.ListPeers() { AddProfile(this, onion) } } diff --git a/go/ui/manager.go b/go/ui/manager.go index b88f70e1..6e991606 100644 --- a/go/ui/manager.go +++ b/go/ui/manager.go @@ -14,11 +14,18 @@ import ( ) func isGroup(id string) bool { - return len(id) == 32 + return len(id) == 32 && !isServer(id) } func isPeer(id string) bool { - return len(id) == 56 + return len(id) == 56 && !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 isServer(id string) bool { + _, ok := the.Peer.GetContactAttribute(id, string(model.KeyTypeServerOnion)) + return ok } func getOrDefault(id, key string, defaultVal string) string { diff --git a/main.go b/main.go index b2b987a7..379cf270 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "cwtch.im/ui/go/ui" "cwtch.im/ui/go/ui/android" "flag" + "fmt" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" "github.com/therecipe/qt/androidextras" @@ -16,6 +17,8 @@ import ( "github.com/therecipe/qt/network" "github.com/therecipe/qt/qml" "github.com/therecipe/qt/quickcontrols2" + "io/ioutil" + "math/rand" "os" "os/user" "path" @@ -65,7 +68,7 @@ func main() { log.ExcludeFromPattern("service.go") log.ExcludeFromPattern("tor/BaseOnionService.go") log.ExcludeFromPattern("applications/auth.go") - log.ExcludeFromPattern("connections/engine.go") + //log.ExcludeFromPattern("connections/engine.go") if os.Getenv("CWTCH_FOLDER") != "" { the.CwtchDir = os.Getenv("CWTCH_FOLDER") @@ -233,10 +236,20 @@ func loadACN() { } var err error the.ACN, err = tor.NewTorACN(the.CwtchDir, torpath) - if err != nil { - // TODO: turn into UI error: status panel? - log.Errorf("Could not start Tor: %v", err) - os.Exit(1) + + if _, ok := err.(*tor.NoTorrcError); ok { + // Stopgap: just dump a basic torrc for now + port := rand.Intn(1000) + 9600 + controlPort := port + 1 + ioutil.WriteFile(path.Join(the.CwtchDir, "tor", "torrc"), []byte(fmt.Sprintf(`SOCKSPort %v \n ControlPort %v`, port, controlPort)), 0600) + the.ACN, err = tor.NewTorACNWithAuth(the.CwtchDir, torpath, controlPort, tor.NullAuthenticator{}) + + if err != nil { + // TODO: turn into UI error: status panel? + log.Errorf("Could not start Tor: %v", err) + os.Exit(1) + } + } } diff --git a/qml/opaque b/qml/opaque index e5e537c7..5c33d6ed 160000 --- a/qml/opaque +++ b/qml/opaque @@ -1 +1 @@ -Subproject commit e5e537c79bad4fa1c21a6544a1865287062bade3 +Subproject commit 5c33d6ed2c46f5fe039fc6fee3cb690cb562cb23 diff --git a/qml/panes/GroupSettingsPane.qml b/qml/panes/GroupSettingsPane.qml index d7ece6c2..f3dbb722 100644 --- a/qml/panes/GroupSettingsPane.qml +++ b/qml/panes/GroupSettingsPane.qml @@ -9,128 +9,177 @@ import QtQuick.Controls 1.4 import "../opaque" as Opaque import "../opaque/styles" import "../utils.js" as Utils +import "../opaque/theme" +import "../const" -ColumnLayout { // groupSettingsPane +Opaque.SettingsList { // groupSettingsPane id: gsp anchors.fill: parent property string groupID property variant addrbook + property bool connected: false + property bool synced: false - Flickable { + settings: Column { anchors.fill: parent - boundsBehavior: Flickable.StopAtBounds - clip:true - contentWidth: tehcol.width - contentHeight: tehcol.height - Column { - id: tehcol - width: gsp.width - leftPadding: 10 - spacing: 5 - - Opaque.ScalingLabel { - text: qsTr("server-label") + ":" - } - - TextField { - id: txtServer - style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } - readOnly: true - } - - Opaque.Button { - icon: "regular/clipboard" - text: qsTr("copy-btn") + Opaque.Setting { + inline: false + label: qsTr("group-name-label") + field: Opaque.ButtonTextField { + id: txtGroupName + readOnly: false + button_text: qsTr("save-btn") + dropShadowColor: Theme.dropShadowPaneColor onClicked: { - gcd.popup("copied-clipboard-notification") + //: notification: copied to clipboard + gcd.saveGroupSettings(groupID, txtGroupName.text) + theStack.title = txtGroupName.text + } + } + } + + Opaque.Setting { + inline: false + label: qsTr("server-label") + + field: Opaque.ButtonTextField { + id: txtServer + readOnly: true + button_text: qsTr("copy-btn") + dropShadowColor: Theme.dropShadowPaneColor + onClicked: { + //: notification: copied to clipboard + gcd.popup(qsTr("copied-to-clipboard-notification")) txtServer.selectAll() txtServer.copy() } } + } - Opaque.ScalingLabel { - text: qsTr("invitation-label") + ":" - } + Opaque.Setting { + inline: false + label: qsTr("invitation-label") - TextField { + field: Opaque.ButtonTextField { id: txtInvitation - style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } readOnly: true - } - - Opaque.Button { - icon: "regular/clipboard" - text: qsTr("copy-btn") - + button_text: qsTr("copy-btn") + dropShadowColor: Theme.dropShadowPaneColor onClicked: { - gcd.popup("copied-clipboard-notification") + //: notification: copied to clipboard + gcd.popup(qsTr("copied-to-clipboard-notification")) txtInvitation.selectAll() txtInvitation.copy() } } + } - Opaque.ScalingLabel{ - text: qsTr("group-name-label") + ":" - } + Opaque.Setting { + property color backgroundColor: parent.color + inline: true + label: qsTr("server-info") + field: Column { + width: parent.width + spacing:10 + RowLayout { + width: parent.width + Layout.fillWidth: true + Opaque.ScalingLabel { + text: gsp.connected ? qsTr("server-connectivity-connected") : qsTr("server-connectivity-disconnected") + Layout.alignment: Qt.AlignLeft + } + Opaque.Icon { + backgroundColor: Theme.backgroundPaneColor + id: serverStatusIcon + height: 18 + width: 18 + Layout.alignment: Qt.AlignRight + iconColor: gsp.connected ? Theme.statusbarOnlineFontColor : Theme.statusbarDisconnectedTorFontColor + source: gcd.assetPath + (gsp.connected ? "core/signal_cellular_4_bar-24px.svg" : "core/signal_cellular_connected_no_internet_4_bar-24px.svg") + } + } + RowLayout { + width: parent.width + Layout.fillWidth: true - TextField { - id: txtGroupName - style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } - } + Opaque.ScalingLabel { + text: gsp.synced ? qsTr("server-synced") : qsTr("server-not-synced") + Layout.alignment: Qt.AlignLeft + } + Opaque.Icon { + id: serverSyncedStatusIcon + backgroundColor: Theme.backgroundPaneColor + height: 18 + width: 18 + Layout.alignment: Qt.AlignRight + iconColor : gsp.synced ? Theme.statusbarOnlineFontColor : Theme.statusbarConnectingFontColor + source: gcd.assetPath + (gsp.synced ? "core/syncing-01.svg" : "core/syncing-03.svg") + } + } - Opaque.Button { - text: qsTr("save-btn") + Opaque.Button { + icon: "regular/hdd" + text: qsTr("view-server-info") + anchors.right: parent.right - onClicked: { - gcd.saveGroupSettings(groupID, txtGroupName.text) - theStack.title = txtGroupName.text - theStack.pane = theStack.messagePane - } - } + onClicked: { + gcd.requestServerSettings(gcd.selectedConversation) + theStack.pane = theStack.serverInfoPane + } + } - //: Invite someone to the group - Opaque.ScalingLabel { text: qsTr("invite-to-group-label") } + } + } - ComboBox { - id: cbInvite - //popup.font.pixelSize: 12 - width: 200 - //font.pixelSize: 20 - style: CwtchComboBoxStyle{} - } - Opaque.Button { - text: qsTr("invite-btn") - onClicked: { - gcd.inviteToGroup(addrbook[cbInvite.currentIndex], groupID) - } - } + Column { + width:parent.width * 0.95 + anchors.horizontalCenter: parent.horizontalCenter Opaque.Button { icon: "regular/trash-alt" text: qsTr("delete-btn") + anchors.right: parent.right + onClicked: { gcd.leaveGroup(groupID) theStack.pane = theStack.emptyPane } } + } - }//end of column with padding - }//end of flickable + } Connections { target: gcd + onUpdateContactStatus: function(_handle, _status, _loading) { + if (txtServer.text == _handle) { + if (_status >= Const.state_connected) { + gsp.connected = true + serverStatusIcon + if (_status != Const.state_synced) { + gsp.synced = false + } else { + gsp.synced = true + } + } else { + gsp.connected = false + gsp.synced = false + } + } + } + onSupplyGroupSettings: function(gid, name, server, invite, accepted, addrbooknames, addrbookaddrs) { gsp.groupID = gid txtGroupName.text = name txtServer.text = server txtInvitation.text = invite - cbInvite.model = addrbooknames.map(function(e){return Utils.htmlEscaped(e)}) + //cbInvite.model = addrbooknames.map(function(e){return Utils.htmlEscaped(e)}) addrbook = addrbookaddrs } } diff --git a/qml/panes/OverlayPane.qml b/qml/panes/OverlayPane.qml index 293ebd96..911b0db1 100644 --- a/qml/panes/OverlayPane.qml +++ b/qml/panes/OverlayPane.qml @@ -33,10 +33,10 @@ ColumnLayout { //: Accept group invite button text: qsTr("accept-group-btn") icon: "regular/heart" - onClicked: { - gcd.acceptGroup(gcd.selectedConversation) - gcd.requestGroupSettings(gcd.selectedConversation) - } + onClicked: { + gcd.acceptGroup(gcd.selectedConversation) + gcd.requestGroupSettings(gcd.selectedConversation) + } } Opaque.Button { @@ -147,5 +147,11 @@ ColumnLayout { overlay.accepted = accepted overlay.inGroup = true } + + onSupplyServerSettings: function(server) { + overlay.name = server + } + + } } diff --git a/qml/panes/ServerInfoPane.qml b/qml/panes/ServerInfoPane.qml new file mode 100644 index 00000000..0ac3e10c --- /dev/null +++ b/qml/panes/ServerInfoPane.qml @@ -0,0 +1,78 @@ +import QtGraphicalEffects 1.0 +import QtQuick 2.7 +import QtQuick.Controls 2.4 +import QtQuick.Controls.Material 2.0 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.11 +import QtQuick.Controls 1.4 + +import "../opaque" as Opaque +import "../opaque/styles" +import "../utils.js" as Utils +import "../opaque/theme" +import "../const" + +Opaque.SettingsList { // groupSettingsPane + id: gsp + anchors.fill: parent + property string serverName + property color backgroundColor: parent.color + + settings: Column { + anchors.fill: parent + + Opaque.Setting { + inline: false + label: qsTr("server-label") + + field: Opaque.ButtonTextField { + id: txtServer + readOnly: true + text: serverName; + button_text: qsTr("copy-btn") + dropShadowColor: Theme.dropShadowPaneColor + onClicked: { + //: notification: copied to clipboard + gcd.popup(qsTr("copied-to-clipboard-notification")) + txtServer.selectAll() + txtServer.copy() + } + } + } + } + + Connections { + target: gcd + + onUpdateContactStatus: function(_handle, _status, _loading) { + if (txtServer.text == _handle) { + if (_status >= Const.state_connected) { + serverStatusIcon.iconColor = Theme.statusbarOnlineFontColor + serverStatusIcon.source = gcd.assetPath + "core/signal_cellular_4_bar-24px.svg" + if (_status != Const.state_synced) { + serverSyncedStatusIcon.iconColor = Theme.statusbarConnectingFontColor + serverSyncedStatusIcon.source = gcd.assetPath + "core/syncing-03.svg" + } else { + serverSyncedStatusIcon.iconColor = Theme.statusbarOnlineFontColor + serverSyncedStatusIcon.source = gcd.assetPath + "core/syncing-01.svg" + } + } else { + serverStatusIcon.iconColor = Theme.statusbarDisconnectedTorFontColor + serverStatusIcon.source = gcd.assetPath + "core/signal_cellular_connected_no_internet_4_bar-24px.svg" + serverSyncedStatusIcon.iconColor = Theme.statusbarDisconnectedTorFontColor + serverSyncedStatusIcon.source = gcd.assetPath + "core/syncing-03.svg" + } + } + } + + onSupplyServerSettings: function(server, key_names, keys) { + gsp.serverName = server; + toolbar.setTitle(qsTr("server-settings")); + console.log("Servers: " + key_names); + for (let i=0; i