Self-Hosted Servers Experiment #359
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module cwtch.im/ui
|
|||
go 1.12
|
||||
|
||||
require (
|
||||
cwtch.im/cwtch v0.4.6
|
||||
cwtch.im/cwtch v0.4.7
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.3.1
|
||||
git.openprivacy.ca/openprivacy/log v1.0.1
|
||||
github.com/c-bata/go-prompt v0.2.3 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -9,8 +9,8 @@ cwtch.im/cwtch v0.4.4 h1:LC9SngDx+iLXFiDK8GUkSxaWqloXQcPijvMPmwt9h80=
|
|||
cwtch.im/cwtch v0.4.4/go.mod h1:10gBkMSqAH95Pz4jTx5mpIHE+dkn+4kRC4BFTxWuQK8=
|
||||
cwtch.im/cwtch v0.4.5 h1:BK4IMqCMf9xNmeLzaVDUbl2bnXdw5fOWXvEGBMTOjXM=
|
||||
cwtch.im/cwtch v0.4.5/go.mod h1:Mh7vQQ3z55+prpX6EuUkg4QNQkBACMoDcgCNBeAH2EY=
|
||||
cwtch.im/cwtch v0.4.6 h1:jQT0WZY0BGS/EKZrtvL48kMYoed00/q1ycvI0u7Dez4=
|
||||
cwtch.im/cwtch v0.4.6/go.mod h1:Mh7vQQ3z55+prpX6EuUkg4QNQkBACMoDcgCNBeAH2EY=
|
||||
cwtch.im/cwtch v0.4.7 h1:y8Roq1L1PAs0FkBDdk+7EUVLCHwyzl+dOEfVu4VX0Ic=
|
||||
cwtch.im/cwtch v0.4.7/go.mod h1:Mh7vQQ3z55+prpX6EuUkg4QNQkBACMoDcgCNBeAH2EY=
|
||||
cwtch.im/tapir v0.2.0 h1:7MkoR5+uEuPW34/O0GZRidnIjq/01Cfm8nl5IRuqpGc=
|
||||
cwtch.im/tapir v0.2.0/go.mod h1:xzzZ28adyUXNkYL1YodcHsAiTt3IJ8Loc29YVn9mIEQ=
|
||||
cwtch.im/tapir v0.2.1 h1:t1YJB9q5sV1A9xwiiwL6WVfw3dwQWLoecunuzT1PQtw=
|
||||
|
|
|
@ -2,7 +2,25 @@ package constants
|
|||
|
||||
import "cwtch.im/cwtch/event"
|
||||
|
||||
// The server manage defines its own events...
|
||||
// The server manager defines its own events, most should be self-explanatory:
|
||||
const (
|
||||
NewServer = event.Type("NewServer")
|
||||
|
||||
// Force a UI update
|
||||
ListServers = event.Type("ListServers")
|
||||
|
||||
// Takes an Onion, used to toggle off/on Server availability
|
||||
StartServer = event.Type("StartServer")
|
||||
StopServer = event.Type("StopServer")
|
||||
|
||||
// Takes an Onion and a AutoStartEnabled boolean
|
||||
AutoStart = event.Type("AutoStart")
|
||||
|
||||
// Get the status of a particular server (takes an Onion)
|
||||
CheckServerStatus = event.Type("CheckServerStatus")
|
||||
ServerStatusUpdate = event.Type("ServerStatusUpdate")
|
||||
)
|
||||
|
||||
const (
|
||||
AutoStartEnabled = event.Field("AutoStartEnabled")
|
||||
)
|
|
@ -1,57 +1,267 @@
|
|||
package servers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/server"
|
||||
"cwtch.im/ui/go/constants"
|
||||
"cwtch.im/ui/go/the"
|
||||
"cwtch.im/ui/go/ui"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
||||
|
||||
// ServerManager is responsible for managing user operated servers
|
||||
type ServerManager struct {
|
||||
servers map[string]server.Server
|
||||
servers sync.Map
|
||||
configDir string
|
||||
acn connectivity.ACN
|
||||
}
|
||||
|
||||
func LaunchServiceManager(gcd *ui.GrandCentralDispatcher) {
|
||||
type serverStatusCache struct {
|
||||
online bool
|
||||
autostart bool
|
||||
messages uint64
|
||||
bundle []byte
|
||||
}
|
||||
|
||||
|
||||
// LaunchServiceManager is responsible for setting up everything relating to managing servers in the UI.
|
||||
func LaunchServiceManager(gcd *ui.GrandCentralDispatcher, acn connectivity.ACN, configDir string) {
|
||||
sm := new(ServerManager)
|
||||
sm.configDir = configDir
|
||||
sm.acn = acn
|
||||
sm.Init(gcd)
|
||||
}
|
||||
|
||||
// initializeServerCache sets up a new cache based on the config. Notably it stores a newly signed keybundle that
|
||||
// the ui uses to allow people to share server key bundles.
|
||||
func initializeServerCache(config server.Config) (*server.Server,serverStatusCache) {
|
||||
newServer := new(server.Server)
|
||||
newServer.Setup(config)
|
||||
newServer.KeyBundle()
|
||||
return newServer, serverStatusCache{
|
||||
false,
|
||||
config.AutoStart,
|
||||
0,
|
||||
newServer.KeyBundle().Serialize(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (sm *ServerManager) Init(gcd *ui.GrandCentralDispatcher) {
|
||||
sm.servers = make(map[string]server.Server)
|
||||
|
||||
q := event.NewQueue()
|
||||
the.AppBus.Subscribe(constants.NewServer, q)
|
||||
the.AppBus.Subscribe(constants.ListServers, q)
|
||||
the.AppBus.Subscribe(constants.ServerStatusUpdate, q)
|
||||
the.AppBus.Subscribe(event.Shutdown, q)
|
||||
|
||||
// NOTE: Servers don't (yet?) benefit from any kind of encryption of their config (unlike peer profiles)
|
||||
// FIXME: We assume servers can be malicious, a compromised server will not be able to derive any additional
|
||||
// metadata from a hosted group (assuming that the server isn't also a group participant) - this logic doesn't
|
||||
// really hold if the server being hosted as part of the UI - however a large pool of self hosted servers also mitigates
|
||||
// many of risks e.g lack of server diversification / availability of servers.
|
||||
// Like many parts of the metadata resistant risk model, it is a compromise
|
||||
log.Debugf("Reading server directory: %v", sm.configDir)
|
||||
os.MkdirAll(sm.configDir,0700)
|
||||
items, _ := ioutil.ReadDir(sm.configDir)
|
||||
for _, item := range items {
|
||||
if item.IsDir() {
|
||||
log.Debugf("Found Server Directory %v / %v", sm.configDir, item.Name())
|
||||
config := server.LoadConfig(path.Join(sm.configDir, item.Name()), "serverconfig")
|
||||
identity := config.Identity()
|
||||
log.Debugf("Launching Server goroutine for %v", identity.Hostname())
|
||||
s,cache := initializeServerCache(config)
|
||||
sm.servers.Store(identity.Hostname(), cache)
|
||||
go sm.runServer(s)
|
||||
} else {
|
||||
log.Debugf("Found non server directory: %v", item.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// Update the UI with all know servers
|
||||
sm.ListServers(gcd)
|
||||
|
||||
for {
|
||||
e := q.Next()
|
||||
|
||||
switch e.EventType {
|
||||
case constants.ListServers: {
|
||||
sm.ListServers(gcd)
|
||||
}
|
||||
case constants.NewServer:
|
||||
sm.NewServer()
|
||||
sm.ListServers(gcd)
|
||||
case constants.ListServers:
|
||||
sm.ListServers(gcd)
|
||||
case constants.ServerStatusUpdate:
|
||||
sm.ListServers(gcd)
|
||||
case event.Shutdown:
|
||||
// we don't need to do anything else here, all the server goroutines will also subscribe to the
|
||||
// shutdown event and return nicely.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Replace with details from actual hosted servers. Right now these values are used to sketch / test out the
|
||||
// UI QML
|
||||
|
||||
// NewServer createa a new server
|
||||
func (sm *ServerManager) NewServer() {
|
||||
log.Debugf("Adding a new Server")
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
|
||||
if err == nil {
|
||||
serverDir := path.Join(sm.configDir, num.String());
|
||||
os.MkdirAll(serverDir,0700)
|
||||
config := server.LoadConfig(serverDir, "serverconfig")
|
||||
identity := config.Identity()
|
||||
s, cache := initializeServerCache(config)
|
||||
sm.servers.Store(identity.Hostname(),cache)
|
||||
go sm.runServer(s)
|
||||
}
|
||||
}
|
||||
|
||||
// runServer sets up an event queue per server to allow them to manage their own state.
|
||||
func (sm *ServerManager) runServer(s * server.Server) {
|
||||
q := event.NewQueue()
|
||||
the.AppBus.Subscribe(constants.StartServer, q)
|
||||
the.AppBus.Subscribe(constants.StopServer, q)
|
||||
the.AppBus.Subscribe(constants.CheckServerStatus, q)
|
||||
the.AppBus.Subscribe(constants.AutoStart, q)
|
||||
the.AppBus.Subscribe(event.Shutdown, q)
|
||||
|
||||
|
||||
identity := s.Identity()
|
||||
|
||||
cache,ok := sm.servers.Load(identity.Hostname())
|
||||
if ok {
|
||||
serverStatusCache := cache.(serverStatusCache)
|
||||
if serverStatusCache.autostart {
|
||||
the.AppBus.Publish(event.NewEvent(constants.StartServer, map[event.Field]string{event.Onion: identity.Hostname()}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log.Debugf("Launching Server %v", identity.Hostname())
|
||||
log.Debugf("Launching Event Bus for Server %v", identity.Hostname())
|
||||
for {
|
||||
e := q.Next()
|
||||
|
||||
switch e.EventType {
|
||||
case constants.StartServer:
|
||||
onion := e.Data[event.Onion]
|
||||
if onion == identity.Hostname() {
|
||||
if running,_ := s.CheckStatus(); running {
|
||||
// we are already running
|
||||
log.Debugf("Server %v Already Running", onion)
|
||||
continue
|
||||
}
|
||||
log.Debugf("Running Server %v", onion)
|
||||
s.Run(sm.acn)
|
||||
|
||||
// TODO Remove this whole blob once we can actually create groups in the UI
|
||||
// This is for developers who want to test their newly created server with a group.
|
||||
group, _ := model.NewGroup(tor.GetTorV3Hostname(identity.PublicKey()))
|
||||
group.SignGroup([]byte{})
|
||||
invite, err := group.Invite([]byte{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Secret Debugging Group for Testing: %v\n", "torv3"+base64.StdEncoding.EncodeToString(invite))
|
||||
cache,ok := sm.servers.Load(onion)
|
||||
if ok {
|
||||
serverStatusCache := cache.(serverStatusCache)
|
||||
serverStatusCache.online = true
|
||||
sm.servers.Store(onion, serverStatusCache)
|
||||
the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{}))
|
||||
}
|
||||
}
|
||||
case constants.StopServer:
|
||||
onion := e.Data[event.Onion]
|
||||
if onion == identity.Hostname() {
|
||||
s.Shutdown()
|
||||
cache,ok := sm.servers.Load(onion)
|
||||
if ok {
|
||||
serverStatusCache := cache.(serverStatusCache)
|
||||
serverStatusCache.online = false
|
||||
sm.servers.Store(onion, serverStatusCache)
|
||||
the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{}))
|
||||
}
|
||||
}
|
||||
case constants.CheckServerStatus:
|
||||
onion := e.Data[event.Onion]
|
||||
if onion == identity.Hostname() {
|
||||
// Kick off a restart
|
||||
if _, err := s.CheckStatus(); err != nil {
|
||||
s.Shutdown()
|
||||
s.Run(sm.acn)
|
||||
}
|
||||
cache, ok := sm.servers.Load(onion)
|
||||
if ok {
|
||||
serverStatusCache := cache.(serverStatusCache)
|
||||
serverStatusCache.messages = uint64(s.GetStatistics().TotalMessages)
|
||||
log.Debugf("Server Statistics %v %v", onion, serverStatusCache.messages)
|
||||
sm.servers.Store(onion, serverStatusCache)
|
||||
the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{}))
|
||||
}
|
||||
}
|
||||
case constants.AutoStart:
|
||||
onion := e.Data[event.Onion]
|
||||
if onion == identity.Hostname() {
|
||||
autostart := e.Data[constants.AutoStartEnabled] == event.True
|
||||
s.ConfigureAutostart(autostart)
|
||||
cache,ok := sm.servers.Load(onion)
|
||||
if ok {
|
||||
serverStatusCache := cache.(serverStatusCache)
|
||||
serverStatusCache.autostart = autostart
|
||||
sm.servers.Store(onion, serverStatusCache)
|
||||
the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{}))
|
||||
}
|
||||
}
|
||||
case event.Shutdown:
|
||||
s.Shutdown()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListServers iterates through the current collection of servers and their associated
|
||||
// cache and sends an update to the UI.
|
||||
func (sm *ServerManager) ListServers(gcd *ui.GrandCentralDispatcher) {
|
||||
log.Debugf("Listing Servers...")
|
||||
gcd.AddServer("Server 1","Server 1","server",0)
|
||||
gcd.AddServer("Server 2","Server 2","server",4)
|
||||
gcd.AddServer("Server 3","Server 3","server",4)
|
||||
}
|
||||
sm.servers.Range(func(k interface{},v interface{}) bool {
|
||||
serverOnion := k.(string)
|
||||
statusCache := v.(serverStatusCache)
|
||||
status := 0
|
||||
if statusCache.online {
|
||||
status = 1
|
||||
}
|
||||
|
||||
func (sm *ServerManager) StartServer(handle string) {
|
||||
// TODO Start the server with the given handle config
|
||||
}
|
||||
// TODO this doesn't allow for an expansion of key types, this whole flow needs rethinking...
|
||||
key_types := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass}
|
||||
var keyNames []string
|
||||
var keys []string
|
||||
|
||||
func (sm *ServerManager) StopServer(handle string) {
|
||||
// TODO Stop the given server
|
||||
keybundle,_ := model.DeserializeAndVerify(statusCache.bundle)
|
||||
|
||||
for _, key_type := range key_types {
|
||||
log.Debugf("Looking up %v %v", key_type, keyNames)
|
||||
if keybundle.HasKeyType(key_type) {
|
||||
key,_ := keybundle.GetKey(key_type)
|
||||
keyNames = append(keyNames, string(key_type))
|
||||
keys = append(keys, string(key))
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Updating Server %v %v %v", serverOnion, status, statusCache.messages)
|
||||
gcd.AddServer(serverOnion,serverOnion,serverOnion, status, statusCache.autostart, "server:"+base64.StdEncoding.EncodeToString(statusCache.bundle), int(statusCache.messages), keyNames, keys)
|
||||
return true
|
||||
})
|
||||
}
|
|
@ -131,8 +131,6 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) {
|
|||
}
|
||||
uiManager.UpdateContactStatus(groupID, int(state), loading)
|
||||
uiManager.UpdateContactStatus(serverOnion, int(state), loading)
|
||||
} else {
|
||||
log.Errorf("found group that is nil :/")
|
||||
}
|
||||
}
|
||||
case event.DeletePeer:
|
||||
|
|
46
go/ui/gcd.go
46
go/ui/gcd.go
|
@ -57,8 +57,13 @@ type GrandCentralDispatcher struct {
|
|||
_ func(failed bool) `signal:"ChangePasswordResponse"`
|
||||
|
||||
// server management
|
||||
_ func(handle, displayname, image string, status int) `signal:"AddServer"`
|
||||
_ func(handle, displayname, image string, status int, autostart bool, bundle string, messages int, key_types []string, keys []string) `signal:"AddServer"`
|
||||
_ func() `signal:"requestServers,auto"`
|
||||
_ func() `signal:"newServer,auto"`
|
||||
_ func(server string) `signal:"startServer,auto"`
|
||||
_ func(server string) `signal:"stopServer,auto"`
|
||||
_ func(server string) `signal:"checkServer,auto"`
|
||||
_ func(server string, enabled bool) `signal:"autostartServer",auto`
|
||||
|
||||
// contact list stuff
|
||||
_ func(handle, displayName, image string, badge, status int, authorization string, loading bool, lastMsgTime int) `signal:"AddContact"`
|
||||
|
@ -388,6 +393,45 @@ func (this *GrandCentralDispatcher) requestServers() {
|
|||
}))
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) newServer() {
|
||||
the.AppBus.Publish(event.NewEvent(constants.NewServer, map[event.Field]string{
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) startServer(onion string) {
|
||||
log.Debugf("Requesting Start Server: %v", onion)
|
||||
the.AppBus.Publish(event.NewEvent(constants.StartServer, map[event.Field]string{
|
||||
event.Onion: onion,
|
||||
}))
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) stopServer(onion string) {
|
||||
log.Debugf("Requesting Stop Server: %v", onion)
|
||||
the.AppBus.Publish(event.NewEvent(constants.StopServer, map[event.Field]string{
|
||||
event.Onion: onion,
|
||||
}))
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) checkServer(onion string) {
|
||||
log.Debugf("Requesting Stop Server: %v", onion)
|
||||
the.AppBus.Publish(event.NewEvent(constants.CheckServerStatus, map[event.Field]string{
|
||||
event.Onion: onion,
|
||||
}))
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) autostartServer(onion string, enabled bool) {
|
||||
log.Debugf("Requesting Autostart Toggle: %v %v", onion, enabled)
|
||||
value := event.False
|
||||
if enabled {
|
||||
value = event.True
|
||||
}
|
||||
the.AppBus.Publish(event.NewEvent(constants.AutoStart, map[event.Field]string{
|
||||
event.Onion: onion,
|
||||
constants.AutoStartEnabled: value,
|
||||
}))
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) {
|
||||
group := the.Peer.GetGroup(groupID)
|
||||
|
||||
|
|
2
main.go
2
main.go
|
@ -323,7 +323,7 @@ func loadNetworkingAndFiles(gcd *ui.GrandCentralDispatcher, service bool, client
|
|||
the.AppBus = the.CwtchApp.GetPrimaryBus()
|
||||
subscribed := make(chan bool)
|
||||
go handlers.App(gcd, subscribed, clientUI)
|
||||
go servers.LaunchServiceManager(gcd)
|
||||
go servers.LaunchServiceManager(gcd, the.ACN, path.Join(the.CwtchDir, "servers"))
|
||||
<-subscribed
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,6 @@ import QtQuick.Window 2.11
|
|||
|
||||
import "../opaque" as Opaque
|
||||
import "../opaque/theme"
|
||||
// import "../styles"
|
||||
|
||||
|
||||
|
||||
Opaque.SettingsList { // Add Profile Pane
|
||||
id: serverAddEditPane
|
||||
|
@ -19,15 +16,22 @@ Opaque.SettingsList { // Add Profile Pane
|
|||
function reset() {
|
||||
serverAddEditPane.server_name = "";
|
||||
serverAddEditPane.server_available = false;
|
||||
serverAddEditPane.server_bundle = ""
|
||||
}
|
||||
|
||||
property string server_name;
|
||||
property bool server_available;
|
||||
property string server_bundle;
|
||||
property bool autostart_server;
|
||||
property int server_messages;
|
||||
|
||||
function load(server_onion, server_name, server_available) {
|
||||
function load(server_onion, server_name, server_available, autostart_server, server_messages, server_bundle) {
|
||||
reset();
|
||||
serverAddEditPane.server_name = server_name;
|
||||
serverAddEditPane.server_available = server_available;
|
||||
serverAddEditPane.server_bundle = server_bundle;
|
||||
serverAddEditPane.autostart_server = autostart_server;
|
||||
serverAddEditPane.server_messages = server_messages;
|
||||
}
|
||||
|
||||
settings: Column {
|
||||
|
@ -36,7 +40,7 @@ Opaque.SettingsList { // Add Profile Pane
|
|||
|
||||
Opaque.ScalingLabel {
|
||||
text: server_name
|
||||
size: 48
|
||||
size: 16
|
||||
}
|
||||
|
||||
Opaque.Setting {
|
||||
|
@ -48,16 +52,64 @@ Opaque.SettingsList { // Add Profile Pane
|
|||
|
||||
isToggled: serverAddEditPane.server_available
|
||||
onToggled: function() {
|
||||
serverAddEditPane.serverAddEditPane = !serverAddEditPane
|
||||
serverAddEditPane.server_available = !serverAddEditPane.server_available
|
||||
if (serverAddEditPane.server_available) {
|
||||
gcd.startServer(serverAddEditPane.server_name)
|
||||
} else {
|
||||
gcd.stopServer(serverAddEditPane.server_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Opaque.Setting {
|
||||
label: qsTr("server-autostart")
|
||||
|
||||
field: Opaque.ToggleSwitch {
|
||||
anchors.right: parent.right
|
||||
|
||||
isToggled: serverAddEditPane.autostart_server
|
||||
onToggled: function() {
|
||||
serverAddEditPane.autostart_server = !serverAddEditPane.autostart_server
|
||||
gcd.autostartServer(serverAddEditPane.server_name, serverAddEditPane.autostart_server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Opaque.Setting {
|
||||
inline: false
|
||||
label: qsTr("server-num-messages")
|
||||
|
||||
field: Opaque.TextField {
|
||||
id: numMessages
|
||||
readOnly: true
|
||||
text: serverAddEditPane.server_messages;
|
||||
}
|
||||
}
|
||||
|
||||
Opaque.Setting {
|
||||
inline: false
|
||||
label: qsTr("server-key-bundle")
|
||||
|
||||
field: Opaque.ButtonTextField {
|
||||
id: txtServerBundle
|
||||
readOnly: true
|
||||
text: serverAddEditPane.server_bundle;
|
||||
button_text: qsTr("copy-btn")
|
||||
dropShadowColor: Theme.dropShadowPaneColor
|
||||
onClicked: {
|
||||
//: notification: copied to clipboard
|
||||
gcd.popup(qsTr("copied-to-clipboard-notification"))
|
||||
txtServerBundle.selectAll()
|
||||
txtServerBundle.copy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Connections { // UPDATE UNREAD MESSAGES COUNTER
|
||||
Connections {
|
||||
target: gcd
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,13 @@ ColumnLayout {
|
|||
gcd.requestServers();
|
||||
}
|
||||
|
||||
onAddServer: function(handle, displayName, image, status) {
|
||||
onAddServer: function(handle, displayName, image, status, autostart, bundle, messages, key_types, keys) {
|
||||
|
||||
// don't add duplicates
|
||||
for (var i = 0; i < serversModel.count; i++) {
|
||||
if (serversModel.get(i)["_handle"] == handle) {
|
||||
serversModel.get(i)["_status"] = status
|
||||
serversModel.get(i)["_messages"] = messages
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +66,9 @@ ColumnLayout {
|
|||
_displayName: displayName,
|
||||
_image: image,
|
||||
_status: status,
|
||||
_bundle: bundle,
|
||||
_autostart: autostart,
|
||||
_messages: messages
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -84,6 +89,9 @@ ColumnLayout {
|
|||
displayName: _displayName
|
||||
image: _image
|
||||
status: _status
|
||||
bundle: _bundle
|
||||
autostart: _autostart
|
||||
messages: _messages
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +113,7 @@ ColumnLayout {
|
|||
}
|
||||
badgeColor: Theme.defaultButtonColor
|
||||
|
||||
onClicked: function(handle) { serverAddEditPane.reset(); parentStack.pane = parentStack.addEditServerPane }
|
||||
onClicked: function(handle) { gcd.newServer() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,13 +14,16 @@ import "../opaque/theme"
|
|||
Opaque.PortraitRow {
|
||||
id: root
|
||||
property int status;
|
||||
property string bundle;
|
||||
property bool autostart;
|
||||
property int messages;
|
||||
|
||||
portraitBorderColor: Theme.portraitOnlineBorderColor
|
||||
portraitColor: Theme.portraitOnlineBackgroundColor
|
||||
nameColor: Theme.portraitOnlineTextColor
|
||||
onionColor: Theme.portraitOnlineTextColor
|
||||
|
||||
badgeColor: status == 4 ? Theme.portraitOnlineBadgeColor : Theme.portraitOfflineBadgeColor
|
||||
badgeColor: status == 1 ? Theme.portraitOnlineBadgeColor : Theme.portraitOfflineBadgeColor
|
||||
badgeVisible: true
|
||||
|
||||
Opaque.Icon {// Edit BUTTON
|
||||
|
@ -42,12 +45,14 @@ Opaque.PortraitRow {
|
|||
size: parent.height * 0.5
|
||||
|
||||
onClicked: {
|
||||
serverAddEditPane.load(handle, displayName, status)
|
||||
gcd.checkServer(handle)
|
||||
serverAddEditPane.load(handle, displayName, status, autostart, messages, bundle)
|
||||
parentStack.pane = parentStack.addEditServerPane
|
||||
}
|
||||
|
||||
onHover: function (hover) {
|
||||
root.isHover = hover
|
||||
gcd.checkServer(handle)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Reference in New Issue