commit c1b11b83283406d363b82815e7a819c5396a8735 Author: Sarah Jamie Lewis Date: Thu Jun 24 15:30:46 2021 -0700 Fresh Repository Commit - Cwtch Beta diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..3455ba6 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,137 @@ +--- +kind: pipeline +type: docker +name: default + +steps: + - name: fetch + image: golang + volumes: + - name: deps + path: /go + commands: + - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor + - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc + - chmod a+x tor + - go get -u golang.org/x/lint/golint + - git fetch --tags + #- export GO111MODULE=on + #- go mod vendor + - go get + # TODO: upgrade to go1.16, remove mod/vendor, add go install for 1.16 + - echo `git describe --tags` > VERSION + - echo `date +%G-%m-%d-%H-%M` > BUILDDATE + + - name: quality + image: golang + volumes: + - name: deps + path: /go + commands: + - go list ./... | xargs go vet + - go list ./... | xargs golint + #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting + + - name: build-linux + image: golang + volumes: + - name: deps + path: /go + commands: + - make linux + + - name: build-android + image: openpriv/android-go-mobile:2021.03 + volumes: + - name: deps + path: /go + commands: + - go mod download + - gomobile init + - make android + + - name: build-windows + image: openpriv/mingw-go:2021.03 + environment: + GOPATH: /go + volumes: + - name: deps + path: /go + commands: + - make windows + + - name: deploy-buildfiles + image: kroniak/ssh-client + environment: + BUILDFILES_KEY: + from_secret: buildfiles_key + secrets: [gogs_account_token] + when: + event: + - push + - tag + status: [ success ] + commands: + - echo $BUILDFILES_KEY > ~/id_rsab64 + - base64 -d ~/id_rsab64 > ~/id_rsa + - chmod 400 ~/id_rsa + - export DIR=libCwtch-go-`cat VERSION`-`cat BUILDDATE` + - mkdir $DIR + - mv libCwtch.so libCwtch.dll cwtch.aar cwtch-sources.jar libCwtch.h $DIR/ + - cd $DIR + - find . -type f -exec sha256sum {} \; > ./../sha256s.txt + - mv ./../sha256s.txt . + - cd .. + - scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/ + + - name: gitea-release + image: plugins/gitea-release + when: + event: tag + settings: + api_key: + from_secret: gogs_account_token + base_url: https://git.openprivacy.ca + files: + - libCwtch.so + - libCwtch.dll + - cwtch.aar + - cwtch-sources.jar + - libCwtch.h + checksum: + - sha256 + - sha512 + + - name: notify-email + image: drillster/drone-email + settings: + host: build.openprivacy.ca + port: 25 + skip_verify: true + from: drone@openprivacy.ca + when: + status: [ failure ] + + - name: notify-gogs + image: openpriv/drone-gogs + when: + event: pull_request + status: [ success, changed, failure ] + environment: + GOGS_ACCOUNT_TOKEN: + from_secret: gogs_account_token + settings: + gogs_url: https://git.openprivacy.ca + +volumes: + # gopath where bin and pkg lives to persist across steps + - name: deps + temp: {} + +trigger: + repo: flutter/libcwtch-go + branch: trunk + event: + - push + - pull_request + - tag diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbb20d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +cwtch-sources.jar +cwtch.aar +libCwtch.h +libCwtch.so +libCwtch.dll diff --git a/BUILDING_DEBUG.md b/BUILDING_DEBUG.md new file mode 100644 index 0000000..b7bedec --- /dev/null +++ b/BUILDING_DEBUG.md @@ -0,0 +1,32 @@ + +*sometimes: rm -rf $GOPATH/pkg* + +*sometimes: rm -rf /tmp/gomobile* + +rm -rf vendor + +go clean --modcache + +go mod download + +go get -u golang.org/x/mobile + +go get -u golang.org/x/mobile/bind + +gomobile clean + +gomobile init + +make android + +# errors + +``` +# runtime/cgo +gcc_android.c:6:10: fatal error: android/log.h: No such file or directory + #include + ^~~~~~~~~~~~~~~ +compilation terminated.``` +``` + +*solution:* run `gomobile init` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..07bb5a3 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: all clean linux android + +DEFAULT_GOAL: linux + +all: linux android + +linux: libCwtch.so + +android: cwtch.aar + +windows: libCwtch.dll + +libCwtch.so: lib.go + ./switch-ffi.sh + go build -buildmode c-shared -o libCwtch.so + +cwtch.aar: lib.go + ./switch-gomobile.sh + gomobile bind -target android + +libCwtch.dll: lib.go + ./switch-ffi.sh + GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-win32 go build -buildmode c-shared -o libCwtch.dll + +clean: + rm -f cwtch.aar cwtch_go.apk libCwtch.h libCwtch.so cwtch-sources.jar libCwtch.dll diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a98146 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Build Instructions... + make linux + make android + +# Using + +## Linux Desktop: + + - `LD_LIBRARY_PATH` set to point to `libCwtch.so` + - or drop a symlink into `/usr/lib` + +## Android + +- copy `cwtch.aar` into `flutter_app/android/cwtch` diff --git a/constants/attributes.go b/constants/attributes.go new file mode 100644 index 0000000..f8b1c98 --- /dev/null +++ b/constants/attributes.go @@ -0,0 +1,23 @@ +package constants + +const SchemaVersion = "schemaVersion" + +const Name = "name" +const LastRead = "last-read" +const Picture = "picture" +const ShowBlocked = "show-blocked" + +const ProfileTypeV1DefaultPassword = "v1-defaultPassword" +const ProfileTypeV1Password = "v1-userPassword" + +// PeerOnline stores state on if the peer believes it is online +const PeerOnline = "peer-online" + +const StateProfilePane = "state-profile-pane" +const StateSelectedConversation = "state-selected-conversation" +const StateSelectedProfileTime = "state-selected-profile-time" + +// Settings +const BlockUnknownPeersSetting = "blockunknownpeers" +const LocaleSetting = "locale" +const ZoomSetting = "zoom" diff --git a/constants/globals.go b/constants/globals.go new file mode 100644 index 0000000..88c80f3 --- /dev/null +++ b/constants/globals.go @@ -0,0 +1,5 @@ +package constants + +// We offer "un-passworded" profiles but our storage encrypts everything with a password. We need an agreed upon +// password to use in that case, that the app case use behind the scenes to password and unlock with +const DefactoPasswordForUnencryptedProfiles = "be gay do crime" diff --git a/constants/server_manager_events.go b/constants/server_manager_events.go new file mode 100644 index 0000000..75982c3 --- /dev/null +++ b/constants/server_manager_events.go @@ -0,0 +1,26 @@ +package constants + +import "cwtch.im/cwtch/event" + +// 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") +) diff --git a/features/contacts/contact_functionality.go b/features/contacts/contact_functionality.go new file mode 100644 index 0000000..1f62219 --- /dev/null +++ b/features/contacts/contact_functionality.go @@ -0,0 +1,41 @@ +package contact + +import ( + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/peer" + "git.openprivacy.ca/flutter/libcwtch-go/features" + "git.openprivacy.ca/openprivacy/connectivity/tor" +) + +// Functionality groups some common UI triggered functions for contacts... +type Functionality struct { +} + +const addContactPrefix = "addcontact" + +const sendMessagePrefix = "sendmessage" + +// FunctionalityGate returns contact.Functionality always +func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) { + return new(Functionality), nil +} + +// SendMessage handles sending messages to contacts +func (pf *Functionality) SendMessage(peer peer.SendMessages, handle string, message string) features.Response { + eventID := peer.SendMessageToPeer(handle, message) + return features.ConstructResponse(sendMessagePrefix, eventID) +} + +// HandleImportString handles contact import strings +func (pf *Functionality) HandleImportString(peer peer.ModifyContactsAndPeers, importString string) features.Response { + if tor.IsValidHostname(importString) { + if peer.GetContact(importString) == nil { + peer.AddContact(importString, importString, model.AuthApproved) + // Implicit Peer Attempt + peer.PeerWithOnion(importString) + return features.ConstructResponse(addContactPrefix, "success") + } + return features.ConstructResponse(addContactPrefix, "contact_already_exists") + } + return features.ConstructResponse(addContactPrefix, "invalid_import_string") +} diff --git a/features/contacts/contact_functionality_addcontact_test.go b/features/contacts/contact_functionality_addcontact_test.go new file mode 100644 index 0000000..a460651 --- /dev/null +++ b/features/contacts/contact_functionality_addcontact_test.go @@ -0,0 +1,124 @@ +package contact + +import ( + "cwtch.im/cwtch/model" + "git.openprivacy.ca/flutter/libcwtch-go/features" + "testing" +) + +const ValidHostname = "openpravyvc6spbd4flzn4g2iqu4sxzsizbtb5aqec25t76dnoo5w7yd" + +type MockPeer struct { + hasContact bool + addContact bool + peerRequest bool +} + +func (m MockPeer) 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 new file mode 100644 index 0000000..9a33418 --- /dev/null +++ b/features/groups/group_functionality.go @@ -0,0 +1,127 @@ +package groups + +import ( + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/peer" + "encoding/base64" + "fmt" + "git.openprivacy.ca/flutter/libcwtch-go/features" + "git.openprivacy.ca/openprivacy/log" + "strings" +) + +const serverPrefix = "server:" +const tofuBundlePrefix = "tofubundle:" +const groupPrefix = "torv3" +const groupExperiment = "tapir-groups-experiment" + +const importBundlePrefix = "importBundle" + +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] { + return new(GroupFunctionality), nil + } + 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.SendMessageToGroupTracked(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 { + 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) { + 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 !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") +} diff --git a/features/groups/group_functionality_test.go b/features/groups/group_functionality_test.go new file mode 100644 index 0000000..8d9c871 --- /dev/null +++ b/features/groups/group_functionality_test.go @@ -0,0 +1,39 @@ +package groups + +import "testing" + +func TestGroupFunctionality_ValidPrefix(t *testing.T) { + gf, _ := ExperimentGate(map[string]bool{groupExperiment: true}) + if gf.ValidPrefix("torv3blahblahblah") == false { + t.Fatalf("torv3 should be a valid prefix") + } + if gf.ValidPrefix("tofubundle:32432423||3242342") == false { + t.Fatalf("tofubundle should be a valid prefix") + } + if gf.ValidPrefix("server:23541233t") == false { + t.Fatalf("server should be a valid prefix") + } + if gf.ValidPrefix("alice!24234") == true { + t.Fatalf("alice should be an invalid predix") + } +} + +func TestGroupFunctionality_IsEnabled(t *testing.T) { + + _, err := ExperimentGate(map[string]bool{}) + + if err == nil { + t.Fatalf("group functionality should be disabled") + } + + _, err = ExperimentGate(map[string]bool{groupExperiment: true}) + + if err != nil { + t.Fatalf("group functionality should be enabled") + } + + _, 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/features/response.go b/features/response.go new file mode 100644 index 0000000..dfbf1b5 --- /dev/null +++ b/features/response.go @@ -0,0 +1,13 @@ +package features + +import "errors" + +// Response is a wrapper to better semantically convey the response type... +type Response error + +const errorSeparator = "." + +// ConstructResponse is a helper function for creating Response structures. +func ConstructResponse(prefix string, error string) Response { + return errors.New(prefix + errorSeparator + error) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4812f20 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.openprivacy.ca/flutter/libcwtch-go + +go 1.15 + +require ( + cwtch.im/cwtch v0.8.11 + git.openprivacy.ca/openprivacy/connectivity v1.4.4 + git.openprivacy.ca/openprivacy/log v1.0.2 + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..21b037e --- /dev/null +++ b/go.sum @@ -0,0 +1,87 @@ +<<<<<<< Updated upstream +cwtch.im/cwtch v0.8.11 h1:nUrd6srjLxInSJ0q1JdmRBhN4RRlZRL+2vIyE/AXkfY= +cwtch.im/cwtch v0.8.11/go.mod h1:D9dtO+WnKqdmufKSfFeFlUYaxLTfE/RtqVe1OD0kiKc= +git.openprivacy.ca/cwtch.im/tapir v0.4.3 h1:sctSfUXHDIqaHfJPDl+5lHtmoEJolQiHTcHZGAe5Qc4= +git.openprivacy.ca/cwtch.im/tapir v0.4.3/go.mod h1:10qEaib5x021zgyZ/97JKWsEpedH5+Vfy2CvB2V+08E= +git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c= +git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU= +git.openprivacy.ca/openprivacy/connectivity v1.4.4 h1:11M3akVCyy/luuhMpZTM1r9Jayl7IHD944Bxsn2FDpU= +git.openprivacy.ca/openprivacy/connectivity v1.4.4/go.mod h1:JVRCIdL+lAG6ohBFWiKeC/MN42nnC0sfFszR9XG6vPQ= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/struCoder/pidusage v0.1.3 h1:pZcSa6asBE38TJtW0Nui6GeCjLTpaT/jAnNP7dUTLSQ= +github.com/struCoder/pidusage v0.1.3/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI= +github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib.go b/lib.go new file mode 100644 index 0000000..a259970 --- /dev/null +++ b/lib.go @@ -0,0 +1,744 @@ +//package cwtch + +package main + +import "C" +import ( + "crypto/rand" + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/peer" + "encoding/json" + "fmt" + "git.openprivacy.ca/flutter/libcwtch-go/constants" + contact "git.openprivacy.ca/flutter/libcwtch-go/features/contacts" + "git.openprivacy.ca/flutter/libcwtch-go/features/groups" + "git.openprivacy.ca/flutter/libcwtch-go/utils" + "git.openprivacy.ca/openprivacy/connectivity" + "runtime" + "strconv" + "strings" + + "encoding/base64" + "git.openprivacy.ca/openprivacy/connectivity/tor" + "git.openprivacy.ca/openprivacy/log" + mrand "math/rand" + "os" + "path" + "path/filepath" + "time" +) + +const ( + // ProfileOnion is an event field that contains the handle for a given profile. + // todo: this should probably be moved back into Cwtch, and renamed ProfileHandle (onions are too tor-specific) + ProfileOnion = event.Field("ProfileOnion") +) + +var application app.Application +var eventHandler *utils.EventHandler +var acnQueue event.Queue +var contactEventsQueue event.Queue +var globalACN connectivity.ACN + +// ChatMessage API currently not officially documented, see +// https://git.openprivacy.ca/cwtch.im/secure-development-handbook/issues/3 +// for latest updates for now +// +// A ChatMessage is the application-layer Cwtch message, delivered to the UI +// as serialized json. +type ChatMessage struct { + O int `json:"o"` + D string `json:"d"` +} + +//export c_StartCwtch +func c_StartCwtch(dir_c *C.char, len C.int, tor_c *C.char, torLen C.int) int8 { + dir := C.GoStringN(dir_c, len) + tor := C.GoStringN(tor_c, torLen) + return int8(StartCwtch(dir, tor)) +} + +// StartCwtch starts cwtch in the library and initlaizes all data structures +// GetAppbusEvents is always safe to use +// the rest of functions are unsafe until the CwtchStarted event has been received indicating StartCwtch has completed +// returns: +// message: CwtchStarted when start up is complete and app is safe to use +// CwtchStartError message when start up fails (includes event.Error data field) +func StartCwtch(appDir string, torPath string) int { + log.SetLevel(log.LevelInfo) + + + log.Infof("StartCwtch(...)") + // Quick hack check that we're being called with the correct params + // On android a stale worker could be calling us with "last apps" directory. Best to abort fast so the app can make a new worker + if runtime.GOOS == "android" { + fh, err := os.Open(torPath) + if err != nil { + log.Errorf("%v", err) + log.Errorf("failed to stat tor, skipping StartCwtch(). potentially normal if the app was reinstalled or the device was restarted; this workorder should get canceled soon") + return 1 + } + _ = fh.Close() + } + go _startCwtch(appDir, torPath) + return 0 +} + +func _startCwtch(appDir string, torPath string) { + log.Infof("application: %v eventHandler: %v acn: %v", application, eventHandler, globalACN) + + if application != nil { + log.Infof("_startCwtch detected existing application; resuming instead of relaunching") + ReconnectCwtchForeground() + return + } + + // Exclude Tapir wire Messages + //(We need a TRACE level) + log.ExcludeFromPattern("service.go") + + // Ensure that the application directory exists...and then initialize settings.. + os.MkdirAll(path.Join(appDir), 0700) + utils.InitGlobalSettingsFile(appDir, constants.DefactoPasswordForUnencryptedProfiles) + + log.Infof("Loading Cwtch Directory %v and tor path: %v", appDir, torPath) + + mrand.Seed(int64(time.Now().Nanosecond())) + port := mrand.Intn(1000) + 9600 + controlPort := port + 1 + + // generate a random password (actually random, stored in memory, for the control port) + key := make([]byte, 64) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + + log.Infof("Creating new EventHandler()") + eventHandler = utils.NewEventHandler() + + log.Infof("making directory %v", appDir) + os.MkdirAll(path.Join(appDir, "/.tor", "tor"), 0700) + tor.NewTorrc().WithSocksPort(port).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)).Build(filepath.Join(appDir, ".tor", "tor", "torrc")) + acn, err := tor.NewTorACNWithAuth(path.Join(appDir, "/.tor"), torPath, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) + if err != nil { + log.Errorf("\nError connecting to Tor replacing with ErrorACN: %v\n", err) + eventHandler.PublishAppEvent(event.NewEventList(utils.CwtchStartError, event.Error, err)) + return + } + globalACN = acn + newApp := app.NewApp(acn, appDir) + acnQueue = event.NewQueue() + newApp.GetPrimaryBus().Subscribe(event.ACNStatus, acnQueue) + newApp.GetPrimaryBus().Subscribe(utils.UpdateGlobalSettings, acnQueue) + newApp.GetPrimaryBus().Subscribe(utils.SetLoggingLevel, acnQueue) + newApp.GetPrimaryBus().Subscribe(event.AppError, acnQueue) + + eventHandler.HandleApp(newApp) + + peer.DefaultEventsToHandle = []event.Type{ + event.EncryptedGroupMessage, + event.NewMessageFromPeer, + event.PeerAcknowledgement, + event.PeerError, + event.SendMessageToPeerError, + event.SendMessageToGroupError, + event.NewGetValMessageFromPeer, + event.PeerStateChange, + event.NewRetValMessageFromPeer, + event.NewGroupInvite, + event.ServerStateChange, + event.ProtocolEngineStopped, + event.RetryServerRequest, + } + settings := utils.ReadGlobalSettings() + settingsJson, _ := json.Marshal(settings) + + newApp.LoadProfiles(constants.DefactoPasswordForUnencryptedProfiles) + 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") + application.GetPrimaryBus().Publish(event.NewEvent(utils.CwtchStarted, map[event.Field]string{})) + application.QueryACNVersion() +} + +//export c_ReconnectCwtchForeground +func c_ReconnectCwtchForeground() { + ReconnectCwtchForeground() +} + +// Like StartCwtch, but StartCwtch has already been called so we don't need to restart Tor etc (probably) +// Do need to re-send initial state tho, eg profiles that are already loaded +func ReconnectCwtchForeground() { + log.Infof("Reconnecting cwtchforeground") + if application == nil { + log.Errorf("ReconnectCwtchForeground: Application is nil, presuming stale thread, EXITING Reconnect\n") + return + } + + // populate profile list + peerList := application.ListPeers() + for onion := range peerList { + eventHandler.Push(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: onion, event.Created: event.False, "Reload": event.True})) + } + + for onion := range peerList { + // fix peerpeercontact message counts + contactList := application.GetPeer(onion).GetContacts() + for _, handle := range contactList { + totalMessages := application.GetPeer(onion).GetContact(handle).Timeline.Len() + len(application.GetPeer(onion).GetContact(handle).UnacknowledgedMessages) + eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{ + event.Identity: onion, + event.RemotePeer: handle, + event.Data: strconv.Itoa(totalMessages), + })) + } + + // fix peergroupcontact message counts + groupList := application.GetPeer(onion).GetGroups() + for _, groupID := range groupList { + totalMessages := application.GetPeer(onion).GetGroup(groupID).Timeline.Len() + len(application.GetPeer(onion).GetGroup(groupID).UnacknowledgedMessages) + eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{ + event.Identity: onion, + event.GroupID: groupID, + event.Data: strconv.Itoa(totalMessages), + })) + } + } + + + application.GetPrimaryBus().Publish(event.NewEvent(utils.CwtchStarted, map[event.Field]string{})) + application.QueryACNStatus() + application.QueryACNVersion() +} + +//export c_SendAppEvent +// A generic method for Rebroadcasting App Events from a UI +func c_SendAppEvent(json_ptr *C.char, json_len C.int) { + eventJson := C.GoStringN(json_ptr, json_len) + SendAppEvent(eventJson) +} + +// SendAppEvent is a generic method for Rebroadcasting App Events from a UI +func SendAppEvent(eventJson string) { + // Convert the Event Json back to a typed Event Struct, this will make the + // rest of the logic nicer. + var new_event event.Event + json.Unmarshal([]byte(eventJson), &new_event) + log.Infof("Event: %v", new_event) + + // We need to update the local cache + // Ideally I think this would be pusgit hed back into Cwtch + switch new_event.EventType { + case utils.UpdateGlobalSettings: + var globalSettings utils.GlobalSettings + err := json.Unmarshal([]byte(new_event.Data[event.Data]), &globalSettings) + if err != nil { + log.Errorf("Error Unmarshalling Settings %v [%v]", err, new_event.Data[event.Data]) + } + 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 { + for onion := range application.ListPeers() { + application.GetPeer(onion).BlockUnknownConnections() + } + } else { + for onion := range application.ListPeers() { + application.GetPeer(onion).AllowUnknownConnections() + } + } + + case utils.SetLoggingLevel: + _, warn := new_event.Data[utils.Warn] + _, error := new_event.Data[utils.Error] + _, debug := new_event.Data[utils.Debug] + _, info := new_event.Data[utils.Info] + // Assign logging level in priority order. The highest logging level wins in the + // event of multiple fields. + if info { + log.SetLevel(log.LevelInfo) + } else if warn { + log.SetLevel(log.LevelWarn) + } else if error { + log.SetLevel(log.LevelError) + } else if debug { + log.SetLevel(log.LevelDebug) + } + default: // do nothing + } +} + +//export c_SendProfileEvent +// A generic method for Rebroadcasting Profile Events from a UI +func c_SendProfileEvent(onion_ptr *C.char, onion_len C.int, json_ptr *C.char, json_len C.int) { + onion := C.GoStringN(onion_ptr, onion_len) + eventJson := C.GoStringN(json_ptr, json_len) + SendProfileEvent(onion, eventJson) +} + +const ( + AddContact = event.Type("AddContact") + ImportString = event.Field("ImportString") +) + +// SendProfileEvent is a generic method for Rebroadcasting Profile Events from a UI +func SendProfileEvent(onion string, eventJson string) { + // Convert the Event Json back to a typed Event Struct, this will make the + // rest of the logic nicer. + var new_event event.Event + json.Unmarshal([]byte(eventJson), &new_event) + log.Infof("Event: %v %v", onion, new_event) + + // Get the correct Peer + peer := application.GetPeer(onion) + if peer == nil { + return + } + + // We need to update the local cache + // Ideally I think this would be pushed back into Cwtch + switch new_event.EventType { + case AddContact: + // Peer Functionality is Always Enabled, so we forgo the existence check... + // TODO: Combine with GroupFunctionality to make a meta-handleimportstring that can do both! + pf, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments) + err := pf.HandleImportString(peer, new_event.Data[ImportString]) + eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: err.Error()})) + case event.SetAttribute: + peer.SetAttribute(new_event.Data[event.Key], new_event.Data[event.Data]) + case event.SetPeerAttribute: + peer.SetContactAttribute(new_event.Data[event.RemotePeer], new_event.Data[event.Key], new_event.Data[event.Data]) + 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) + application.GetEventBus(onion).Publish(new_event) + } +} + +//export c_GetAppBusEvent +func c_GetAppBusEvent() *C.char { + return C.CString(GetAppBusEvent()) +} + +// GetAppBusEvent blocks until an event +func GetAppBusEvent() string { + log.Debugf("appbusevent called") + for eventHandler == nil { + log.Debugf("waiting for eventHandler != nil") + time.Sleep(time.Second) + } + + var json = "" + for json == "" { + log.Debugf("waiting for json != ''") + json = eventHandler.GetNextEvent() + } + log.Debugf("appbusevent: %v", json) + return json +} + +type Profile struct { + Name string `json:"name"` + Onion string `json:"onion"` + ImagePath string `json:"imagePath"` +} + +//export c_CreateProfile +func c_CreateProfile(nick_ptr *C.char, nick_len C.int, pass_ptr *C.char, pass_len C.int) { + CreateProfile(C.GoStringN(nick_ptr, nick_len), C.GoStringN(pass_ptr, pass_len)) +} + +func CreateProfile(nick, pass string) { + if pass == constants.DefactoPasswordForUnencryptedProfiles { + application.CreateTaggedPeer(nick, pass, constants.ProfileTypeV1DefaultPassword) + } else { + application.CreateTaggedPeer(nick, pass, constants.ProfileTypeV1Password) + } +} + +//export c_LoadProfiles +func c_LoadProfiles(passwordPtr *C.char, passwordLen C.int) { + LoadProfiles(C.GoStringN(passwordPtr, passwordLen)) +} + +func LoadProfiles(pass string) { + application.LoadProfiles(pass) +} + +//export c_ContactEvents +func c_ContactEvents() *C.char { + return C.CString(ContactEvents()) +} + +func ContactEvents() string { + select { + case myevent := <-contactEventsQueue.OutChan(): + return fmt.Sprintf("%v", myevent) + default: + return "" + } +} + +//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)) +} + +// AcceptContact 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) { + 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) + } +} + +//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 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()) + } +} + +//export c_UpdateMessageFlags +func c_UpdateMessageFlags(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, mIdx C.int, message_flags C.ulong) { + profile := C.GoStringN(profile_ptr, profile_len) + handle := C.GoStringN(handle_ptr, handle_len) + UpdateMessageFlags(profile, handle, int(mIdx), int64(message_flags)) +} + +// UpdateMessageFlags sets the messages flags on a given message for a given profile. +// gomobile doesn't support uint64...so here we are.... +func UpdateMessageFlags(profileOnion, handle string, mIdx int, flags int64) { + profile := application.GetPeer(profileOnion) + if profile != nil { + profile.UpdateMessageFlags(handle, mIdx, uint64(flags)) + } else { + log.Errorf("called updatemessageflags with invalid profile onion") + } +} + +//export c_GetMessage +func c_GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char { + profile := C.GoStringN(profile_ptr, profile_len) + handle := C.GoStringN(handle_ptr, handle_len) + return C.CString(GetMessage(profile, handle, int(message_index))) +} + +// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI. +type EnhancedMessage struct { + model.Message + ContactImage string +} + +func GetMessage(profileOnion, handle string, message_index int) string { + profile := application.GetPeer(profileOnion) + ph := utils.NewPeerHelper(profile) + var message EnhancedMessage + if ph.IsGroup(handle) { + if len(profile.GetGroup(handle).Timeline.Messages) > message_index { + message.Message = profile.GetGroup(handle).Timeline.Messages[message_index] + message.ContactImage = ph.GetProfilePic(message.Message.PeerID) + } else { + // Message Index Request exceeded Timeline, most likely reason is this is a request for an + // unacknowledged sent message (it can take a many seconds for a message to be confirmed in the worst + // case). + offset := message_index - len(profile.GetGroup(handle).Timeline.Messages) + if len(profile.GetGroup(handle).UnacknowledgedMessages) > offset { + message.Message = profile.GetGroup(handle).UnacknowledgedMessages[offset] + message.ContactImage = ph.GetProfilePic(message.Message.PeerID) + } else { + log.Errorf("Couldn't find message in timeline or unacked messages, probably transient threading issue, but logging for visibility..") + } + } + } else { + if message_index < len(profile.GetContact(handle).Timeline.Messages) { + message.Message = profile.GetContact(handle).Timeline.Messages[message_index] + message.ContactImage = ph.GetProfilePic(handle) + } else { + log.Errorf("peerpeercontact getmessage out of range; sending counter resync just in case") + eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{ + event.Identity: profileOnion, + event.RemotePeer: handle, + event.Data: strconv.Itoa(len(profile.GetContact(handle).Timeline.Messages)), + })) + } + } + bytes, _ := json.Marshal(message) + return string(bytes) +} + + +//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) { + 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) +} + +func SendMessage(profileOnion, handle, 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) + } + } else { + contactHandler, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments) + contactHandler.SendMessage(profile, handle, 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) { + 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) +} + +// Send 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) { + 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)) + } +} + +//export c_ResetTor +func c_ResetTor() { + ResetTor() +} + +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, name_ptr *C.char, name_len C.int) { + profile := C.GoStringN(profile_ptr, profile_len) + server := C.GoStringN(server_ptr, server_len) + name := C.GoStringN(name_ptr, name_len) + CreateGroup(profile, server, name) +} + +// CreateGroup takes in a profile and server in addition to a name and creates a new group. +func CreateGroup(profile string, server string, name 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) + // set the group name + peer.SetGroupAttribute(gid, attr.GetLocalScope("name"), name) + } else { + log.Errorf("error creating group or %v on server %v: %v", profile, server, err) + } + } +} + +//export c_DeleteProfile +func c_DeleteProfile(profile_ptr *C.char, profile_len C.int, password_ptr *C.char, password_len C.int) { + profile := C.GoStringN(profile_ptr, profile_len) + password := C.GoStringN(password_ptr, password_len) + DeleteProfile(profile, password) +} + +// DeleteProfile deletes a profile given the right password +func DeleteProfile(profile string, password string) { + + // allow a blank password to delete "unencrypted" accounts... + if password == "" { + password = constants.DefactoPasswordForUnencryptedProfiles + } + + application.DeletePeer(profile, password) +} + +//export c_LeaveConversation +func c_LeaveConversation(profile_ptr *C.char, profile_len C.int, contact_ptr *C.char, contact_len C.int) { + profile := C.GoStringN(profile_ptr, profile_len) + contact := C.GoStringN(contact_ptr, contact_len) + LeaveConversation(profile, contact) +} + +// LeaveConversation forces profile to leave the peer +func LeaveConversation(profile string, contact string) { + peer := application.GetPeer(profile) + peer.DeleteContact(contact) +} + +//export c_LeaveGroup +func c_LeaveGroup(profile_ptr *C.char, profile_len C.int, group_ptr *C.char, group_len C.int) { + profile := C.GoStringN(profile_ptr, profile_len) + groupID := C.GoStringN(group_ptr, group_len) + LeaveGroup(profile, groupID) +} + +// LeaveGroup forces profile to leave the group groupID +func LeaveGroup(profile string, groupID string) { + peer := application.GetPeer(profile) + _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) + if err == nil { + peer.DeleteGroup(groupID) + } +} + +//export c_ImportBundle +func c_ImportBundle(profile_ptr *C.char, profile_len C.int, bundle_ptr *C.char, bundle_len C.int) { + profile := C.GoStringN(profile_ptr, profile_len) + name := C.GoStringN(bundle_ptr, bundle_len) + ImportBundle(profile, name) +} + +// ImportBundle takes in a handle to a profile and an invite string which could have one of many +// 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()})) + + // 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 + } + } + eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: response.Error()})) +} + +//export c_SetGroupAttribute +func c_SetGroupAttribute(profile_ptr *C.char, profile_len C.int, group_ptr *C.char, group_len C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) { + profileOnion := C.GoStringN(profile_ptr, profile_len) + groupHandle := C.GoStringN(group_ptr, group_len) + key := C.GoStringN(key_ptr, key_len) + value := C.GoStringN(val_ptr, val_len) + SetGroupAttribute(profileOnion, groupHandle, key, value) +} + +// SetGroupAttribute provides a wrapper around profile.SetGroupAttribute, gated by global experiments... +func SetGroupAttribute(profileOnion string, groupHandle string, key string, value string) { + profile := application.GetPeer(profileOnion) + _, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments) + if err == nil { + profile.SetGroupAttribute(groupHandle, key, value) + } +} + +//export c_ShutdownCwtch +func c_ShutdownCwtch() { + ShutdownCwtch() +} + +// ShutdownCwtch is a safe way to shutdown any active cwtch applications and associated ACNs +func ShutdownCwtch() { + if application != nil && globalACN != nil { + // Kill the isolate + eventHandler.Push(event.NewEvent(event.Shutdown, map[event.Field]string{})) + + // Allow for the shutdown events to go through and then purge everything else... + log.Infof("Shutting Down Application...") + application.Shutdown() + log.Infof("Shutting Down ACN...") + globalACN.Close() + log.Infof("Library Shutdown Complete!") + // do not remove - important for state checks elsewhere + application = nil + globalACN = nil + eventHandler = nil + } +} + +// Leave as is, needed by ffi +func main() {} diff --git a/quality.sh b/quality.sh new file mode 100755 index 0000000..c913c92 --- /dev/null +++ b/quality.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +echo "Checking code quality (you want to see no output here)" +echo "" + +echo "Vetting:" +go list ./... | xargs go vet + +echo "" +echo "Linting:" + +go list ./... | xargs golint + + +echo "Time to format" +gofmt -l -s -w . + +# ineffassign (https://github.com/gordonklaus/ineffassign) +echo "Checking for ineffectual assignment of errors (unchecked errors...)" +ineffassign . + +# misspell (https://github.com/client9/misspell/cmd/misspell) +echo "Checking for misspelled words..." +misspell . | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea" diff --git a/switch-ffi.sh b/switch-ffi.sh new file mode 100755 index 0000000..7985e9f --- /dev/null +++ b/switch-ffi.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +sed -i "s/^package cwtch/\/\/package cwtch/" lib.go +sed -i "s/^\/\/package main/package main/" lib.go +sed -i "s/^\/\/func main()/func main()/" lib.go \ No newline at end of file diff --git a/switch-gomobile.sh b/switch-gomobile.sh new file mode 100755 index 0000000..be90765 --- /dev/null +++ b/switch-gomobile.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +sed -i "s/^\/\/package cwtch/package cwtch/" lib.go +sed -i "s/^package main/\/\/package main/" lib.go +sed -i "s/^func main()/\/\/func main()/" lib.go diff --git a/utils/contacts.go b/utils/contacts.go new file mode 100644 index 0000000..2c9aa66 --- /dev/null +++ b/utils/contacts.go @@ -0,0 +1,15 @@ +package utils + +type Contact struct { + Name string `json:"name"` + Onion string `json:"onion"` + Status string `json:"status"` + Picture string `json:"picture"` + Authorization string `json:"authorization"` + SaveHistory string `json:"saveConversationHistory"` + Messages int `json:"numMessages"` + Unread int `json:"numUnread"` + LastMessage string `json:"lastMsgTime"` + IsGroup bool `json:"isGroup"` + GroupServer string `json:"groupServer"` +} diff --git a/utils/eventHandler.go b/utils/eventHandler.go new file mode 100644 index 0000000..02eda75 --- /dev/null +++ b/utils/eventHandler.go @@ -0,0 +1,379 @@ +package utils + +import ( + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/app/plugins" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/protocol/connections" + "encoding/json" + "git.openprivacy.ca/flutter/libcwtch-go/constants" + "git.openprivacy.ca/flutter/libcwtch-go/features/groups" + "git.openprivacy.ca/openprivacy/log" + "strconv" +) +import "cwtch.im/cwtch/event" + +type EventProfileEnvelope struct { + Event event.Event + Profile string +} + +type EventHandler struct { + app app.Application + appBusQueue event.Queue + profileEvents chan EventProfileEnvelope +} + +func NewEventHandler() *EventHandler { + eh := &EventHandler{app: nil, appBusQueue: event.NewQueue(), profileEvents: make(chan EventProfileEnvelope)} + return eh +} + +// PublishAppEvent is a way for libCwtch-go to publish an event for consumption by a UI before a Cwtch app has been initialized +// Main use: to signal an error before a cwtch app could be created +func (eh *EventHandler) PublishAppEvent(event event.Event) { + eh.appBusQueue.Publish(event) +} + +func (eh *EventHandler) HandleApp(application app.Application) { + eh.app = application + application.GetPrimaryBus().Subscribe(event.NewPeer, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.PeerError, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.PeerDeleted, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.Shutdown, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.AppError, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.ACNStatus, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.ReloadDone, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.ACNVersion, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(UpdateGlobalSettings, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(CwtchStarted, eh.appBusQueue) +} + +func (eh *EventHandler) GetNextEvent() string { + appChan := eh.appBusQueue.OutChan() + + select { + case e := <-appChan: + return eh.handleAppBusEvent(&e) + case ev := <-eh.profileEvents: + return eh.handleProfileEvent(&ev) + } +} + +// handleAppBusEvent enriches AppBus events so they are usable with out further data fetches +func (eh *EventHandler) handleAppBusEvent(e *event.Event) string { + log.Debugf("New AppBus Event to Handle: %v", e) + if eh.app != nil { + switch e.EventType { + case event.ACNStatus: + if e.Data[event.Progress] == "100" { + for onion := range eh.app.ListPeers() { + // launch a listen thread (internally this does a check that the protocol engine is not listening) + // and as such is safe to call. + eh.app.GetPeer(onion).Listen() + } + } + case event.NewPeer: + onion := e.Data[event.Identity] + profile := eh.app.GetPeer(e.Data[event.Identity]) + log.Debug("New Peer Event: %v", e) + + if e.Data["Reload"] != event.True { + eh.startHandlingPeer(onion) + } + + tag,isTagged := profile.GetAttribute(app.AttributeTag) + if isTagged { + e.Data[app.AttributeTag] = tag + } else { + // Assume encrypted for non-tagged profiles - this isn't always true, but all post-beta profiles + // are tagged on creation. + e.Data[app.AttributeTag] = constants.ProfileTypeV1Password + } + + if e.Data[event.Created] == event.True { + name, _ := profile.GetAttribute(attr.GetLocalScope(constants.Name)) + profile.SetAttribute(attr.GetPublicScope(constants.Name), name) + profile.SetAttribute(attr.GetPublicScope(constants.Picture), ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro))) + } + if e.Data[event.Status] != event.StorageRunning || e.Data[event.Created] == event.True { + profile.SetAttribute(attr.GetLocalScope(constants.PeerOnline), event.False) + eh.app.AddPeerPlugin(onion, plugins.CONNECTIONRETRY) + eh.app.AddPeerPlugin(onion, plugins.NETWORKCHECK) + + // If the user has chosen to block unknown profiles + // then explicitly configure the protocol engine to do so.. + if ReadGlobalSettings().BlockUnknownConnections { + profile.BlockUnknownConnections() + } else { + // For completeness + profile.AllowUnknownConnections() + } + + // Start up the Profile + profile.Listen() + profile.StartPeersConnections() + if _, err := groups.ExperimentGate(ReadGlobalSettings().Experiments); err == nil { + profile.StartServerConnections() + } + } + + nick, exists := profile.GetAttribute(attr.GetPublicScope(constants.Name)) + if !exists { + nick = onion + } + + picVal, ok := profile.GetAttribute(attr.GetPublicScope(constants.Picture)) + if !ok { + picVal = ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro)) + } + pic, err := StringToImage(picVal) + if err != nil { + pic = NewImage(RandomProfileImage(onion), TypeImageDistro) + } + picPath := GetPicturePath(pic) + + //tag, _ := profile.GetAttribute(app.AttributeTag) + + online, _ := profile.GetAttribute(attr.GetLocalScope(constants.PeerOnline)) + + e.Data[constants.Name] = nick + e.Data[constants.Picture] = picPath + 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() { + groupHandler, err := groups.ExperimentGate(ReadGlobalSettings().Experiments) + if err == nil { + servers = append(servers, groupHandler.GetServerInfo(contact, profile)) + } + continue + } + + contactInfo := profile.GetContact(contact) + ph := NewPeerHelper(profile) + name := ph.GetNick(contact) + cpicPath := ph.GetProfilePic(contact) + saveHistory, set := contactInfo.GetAttribute(event.SaveHistoryKey) + if !set { + saveHistory = event.DeleteHistoryDefault + } + 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, + }) + } + + // 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 + } + + ph := NewPeerHelper(profile) + cpicPath := ph.GetProfilePic(groupId) + + authorization := model.AuthUnknown + if group.Accepted { + authorization = model.AuthApproved + } + + contacts = append(contacts, Contact{ + Name: ph.GetNick(groupId), + Onion: group.GroupID, + Status: group.State, + Picture: cpicPath, + Authorization: string(authorization), + SaveHistory: event.SaveHistoryConfirmed, + Messages: group.Timeline.Len(), + Unread: 0, + LastMessage: strconv.Itoa(getLastMessageTime(&group.Timeline)), + IsGroup: true, + GroupServer: group.GroupServer, + }) + } + + 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.Debugf("contactsJson %v", e.Data["ContactsJson"]) + } + } + + json, _ := json.Marshal(e) + return string(json) +} + +// handleProfileEvent enriches Profile events so they are usable with out further data fetches +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) + 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"]) + 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]) + case event.PeerAcknowledgement: + // No enrichement required + case event.PeerCreated: + handle := ev.Event.Data[event.RemotePeer] + err := EnrichNewPeer(handle, ph, ev) + 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]) + 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) + 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) + return "" + } + case event.PeerStateChange: + cxnState := connections.ConnectionStateToType()[ev.Event.Data[event.ConnectionState]] + contact := peer.GetContact(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) + return "" + } + + if contact != nil { + // No enrichment needed + //uiManager.UpdateContactStatus(contact.Onion, int(cxnState), false) + if cxnState == connections.AUTHENTICATED { + // if known and authed, get vars + peer.SendGetValToPeer(ev.Event.Data[event.RemotePeer], attr.PublicScope, constants.Name) + peer.SendGetValToPeer(ev.Event.Data[event.RemotePeer], attr.PublicScope, constants.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] + 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 && scope == attr.PublicScope { + if _, exists := peer.GetContactAttribute(onion, attr.GetLocalScope(path)); exists { + // we have a locally set ovverride, don't pass this remote set public scope update to UI + return "" + } + } + } + } + + json, _ := json.Marshal(unwrap(ev)) + return string(json) +} + +func unwrap(original *EventProfileEnvelope) *event.Event { + unwrapped := &original.Event + unwrapped.Data["ProfileOnion"] = original.Profile + return unwrapped +} + +func (eh *EventHandler) startHandlingPeer(onion string) { + eventBus := eh.app.GetEventBus(onion) + q := event.NewQueue() + eventBus.Subscribe(event.NewMessageFromPeer, q) + eventBus.Subscribe(event.PeerAcknowledgement, q) + eventBus.Subscribe(event.DeleteContact, q) + eventBus.Subscribe(event.AppError, q) + eventBus.Subscribe(event.IndexedAcknowledgement, q) + eventBus.Subscribe(event.IndexedFailure, q) + eventBus.Subscribe(event.NewMessageFromGroup, q) + eventBus.Subscribe(event.GroupCreated, q) + eventBus.Subscribe(event.NewGroup, q) + eventBus.Subscribe(event.AcceptGroupInvite, q) + eventBus.Subscribe(event.SetGroupAttribute, q) + eventBus.Subscribe(event.DeleteGroup, q) + eventBus.Subscribe(event.SendMessageToGroupError, q) + 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) + eventBus.Subscribe(event.NewRetValMessageFromPeer, q) + eventBus.Subscribe(event.SetAttribute, q) + + go eh.forwardProfileMessages(onion, q) + +} + +func (eh *EventHandler) forwardProfileMessages(onion string, q event.Queue) { + log.Infof("Launching Forwarding Goroutine for %v", onion) + // TODO: graceful shutdown, via an injected event of special QUIT type exiting loop/go routine + for { + e := q.Next() + ev := EventProfileEnvelope{Event: e, Profile: onion} + eh.profileEvents <- ev + if ev.Event.EventType == event.Shutdown { + return + } + } +} + +func (eh *EventHandler) Push(newEvent event.Event) { + eh.appBusQueue.Publish(newEvent) +} diff --git a/utils/imageType.go b/utils/imageType.go new file mode 100644 index 0000000..72faa1e --- /dev/null +++ b/utils/imageType.go @@ -0,0 +1,34 @@ +package utils + +import "encoding/json" + +// Image types we support +const ( + // TypeImageDistro is a reletive path to any of the distributed images in cwtch/ui in the assets folder + TypeImageDistro = "distro" + // TypeImageComposition will be an face image composed of a recipe of parts like faceType, eyeType, etc + TypeImageComposition = "composition" +) + +type image struct { + Val string + T string +} + +func NewImage(val, t string) *image { + return &image{val, t} +} + +func StringToImage(str string) (*image, error) { + var img image + err := json.Unmarshal([]byte(str), &img) + if err != nil { + return nil, err + } + return &img, nil +} + +func ImageToString(img *image) string { + bytes, _ := json.Marshal(img) + return string(bytes) +} diff --git a/utils/logging.go b/utils/logging.go new file mode 100644 index 0000000..8a8d713 --- /dev/null +++ b/utils/logging.go @@ -0,0 +1,18 @@ +package utils + +import "cwtch.im/cwtch/event" + +// An event to set the logging level dynamically from the UI +const ( + SetLoggingLevel = event.Type("SetLoggingLevel") +) + +// Logging Levels as Event Fields. Note: Unlike most event we don't cae about +// the *value* of the field, only the presence. If more than one of these fields is +// present in a single SetLoggingLevel event then the highest logging level is used. INFO < WARN < ERROR < DEBUG +const ( + Warn = event.Field("Warn") + Error = event.Field("Error") + Debug = event.Field("Debug") + Info = event.Field("Info") +) diff --git a/utils/manager.go b/utils/manager.go new file mode 100644 index 0000000..1059986 --- /dev/null +++ b/utils/manager.go @@ -0,0 +1,369 @@ +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/flutter/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 +} + +/* +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 + "]" + // re-request + p.peer.SendGetValToPeer(id, attr.PublicScope, 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()) +} + +/* +// AddProfile adds a new profile to the UI +func AddProfile(gcd *GrandCentralDispatcher, handle string) { + p := the.CwtchApp.GetPeer(handle) + if p != nil { + nick, exists := p.GetAttribute(attr.GetPublicScope(constants.Name)) + if !exists { + nick = handle + } + + picVal, ok := p.GetAttribute(attr.GetPublicScope(constants.Picture)) + if !ok { + picVal = ImageToString(NewImage(RandomProfileImage(handle), TypeImageDistro)) + } + pic, err := StringToImage(picVal) + if err != nil { + pic = NewImage(RandomProfileImage(handle), TypeImageDistro) + } + picPath := getPicturePath(pic) + + tag, _ := p.GetAttribute(app.AttributeTag) + + online, _ := p.GetAttribute(attr.GetLocalScope(constants.PeerOnline)) + + log.Debugf("AddProfile %v %v %v %v %v\n", handle, nick, picPath, tag, online) + gcd.AddProfile(handle, nick, picPath, tag, online == event.True) + } +}*/ +/* +type manager struct { + gcd *GrandCentralDispatcher + profile string +} + +// Manager is a middleware helper for entities like peer event listeners wishing to trigger ui changes (via the gcd) +// each manager is for one profile/peer +// manager takes minimal arguments and builds the full struct of data (usually pulled from a cwtch peer) required to call the GCD to perform the ui action +// manager also performs call filtering based on UI state: users of manager can safely always call it on events and not have to worry about weather the relevant ui is active +// ie: you can always safely call AddMessage even if in the ui a different profile is selected. manager will check with gcd, and if the correct conditions are not met, it will not call on gcd to update the ui incorrectly +type Manager interface { + Acknowledge(handle, mID string) + AddContact(Handle string) + AddSendMessageError(peer string, signature string, err string) + AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) + + ReloadProfiles() + + UpdateContactDisplayName(handle string) + UpdateContactPicture(handle string) + UpdateContactStatus(handle string, status int, loading bool) + UpdateContactAttribute(handle, key, value string) + + ChangePasswordResponse(error bool) + + AboutToAddMessage() + MessageJustAdded() + StoreAndNotify(peer.CwtchPeer, string, string, time.Time, string) + + UpdateNetworkStatus(online bool) +} + +// NewManager returns a new Manager interface for a profile to the gcd +func NewManager(profile string, gcd *GrandCentralDispatcher) Manager { + return &manager{gcd: gcd, profile: profile} +} + + +*/ +// 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 { + log.Infof("Enriching New Peer %v", handle) + 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["nick"] = ph.GetNick(handle) + ev.Event.Data["status"] = strconv.Itoa(int(connections.ConnectionStateToType()[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 +} + +/* +// AddSendMessageError adds an error not and icon to a message in a conversation in the ui for the message identified by the peer/sig combo +func (this *manager) AddSendMessageError(peer string, signature string, err string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(peer, func() { + log.Debugf("Received Error Sending Message: %v", err) + // FIXME: Sometimes, for the first Peer message we send our error beats our message to the UI + time.Sleep(time.Second * 1) + this.gcd.GroupSendError(signature, err) + }) + }) +} + +func (this *manager) AboutToAddMessage() { + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) +} + +func (this *manager) MessageJustAdded() { + this.gcd.TimelineInterface.RequestEIR() +}*/ + +/* +// AddMessage adds a message to the message pane for the supplied conversation if it is active +func (this *manager) AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(handle, func() { + updateLastReadTime(handle) + // If the message is not from the user then add it, otherwise, just acknowledge. + if !fromMe || !Acknowledged { + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num() - 1) + this.gcd.TimelineInterface.RequestEIR() + } else { + this.gcd.Acknowledged(messageID) + } + }) + this.gcd.IncContactUnreadCount(handle) + }) + if !fromMe { + this.gcd.Notify(handle) + } +} + +func (this *manager) ReloadProfiles() { + this.gcd.reloadProfileList() +} + +// UpdateContactDisplayName updates a contact's display name in the contact list and conversations +func (this *manager) UpdateContactDisplayName(handle string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactDisplayName(handle, GetNick(handle)) + }) +} + +// UpdateContactPicture updates a contact's picture in the contact list and conversations +func (this *manager) UpdateContactPicture(handle string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactPicture(handle, GetProfilePic(handle)) + }) +} + +// UpdateContactAttribute update's a contacts attribute in the ui +func (this *manager) UpdateContactAttribute(handle, key, value string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactAttribute(handle, key, value) + }) +} + +func (this *manager) ChangePasswordResponse(error bool) { + this.gcd.ChangePasswordResponse(error) +} + +func (this *manager) UpdateNetworkStatus(online bool) { + this.gcd.UpdateProfileNetworkStatus(this.profile, online) +} +*/ diff --git a/utils/settings.go b/utils/settings.go new file mode 100644 index 0000000..2356559 --- /dev/null +++ b/utils/settings.go @@ -0,0 +1,110 @@ +package utils + +import ( + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/storage/v1" + + "encoding/json" + "git.openprivacy.ca/openprivacy/log" + "io/ioutil" + "os" + "path" +) + +const ( + CwtchStarted = event.Type("CwtchStarted") + CwtchStartError = event.Type("CwtchStartError") + UpdateGlobalSettings = event.Type("UpdateGlobalSettings") +) + +var GlobalSettingsFile v1.FileStore + +const GlobalSettingsFilename = "ui.globals" +const saltFile = "SALT" + +type GlobalSettings struct { + Locale string + Theme string + PreviousPid int64 + ExperimentsEnabled bool + Experiments map[string]bool + BlockUnknownConnections bool + StateRootPane int + FirstTime bool + UIColumnModePortrait string + UIColumnModeLandscape string +} + +var DefaultGlobalSettings = GlobalSettings{ + Locale: "en", + Theme: "dark", + PreviousPid: -1, + ExperimentsEnabled: false, + Experiments: make(map[string]bool), + StateRootPane: 0, + FirstTime: true, + BlockUnknownConnections: false, + UIColumnModePortrait: "DualpaneMode.Single", + UIColumnModeLandscape: "DualpaneMode.CopyPortrait", +} + +func InitGlobalSettingsFile(directory string, password string) error { + var key [32]byte + salt, err := ioutil.ReadFile(path.Join(directory, saltFile)) + if err != nil { + log.Infof("Could not find salt file: %v (creating a new settings file)", err) + var newSalt [128]byte + key, newSalt, err = v1.CreateKeySalt(password) + if err != nil { + log.Errorf("Could not initialize salt: %v", err) + return err + } + os.Mkdir(directory, 0700) + err := ioutil.WriteFile(path.Join(directory, saltFile), newSalt[:], 0600) + if err != nil { + log.Errorf("Could not write salt file: %v", err) + return err + } + } else { + key = v1.CreateKey(password, salt) + } + + GlobalSettingsFile = v1.NewFileStore(directory, GlobalSettingsFilename, key) + log.Infof("initialized global settings file: %v", GlobalSettingsFile) + return nil +} + +func ReadGlobalSettings() *GlobalSettings { + settings := DefaultGlobalSettings + + if GlobalSettingsFile == nil { + log.Errorf("Global Settings File was not Initialized Properly") + return &settings + } + + settingsBytes, err := GlobalSettingsFile.Read() + if err != nil { + log.Infof("Could not read global ui settings: %v (assuming this is a first time app deployment...)", err) + return &settings //firstTime = true + } + + err = json.Unmarshal(settingsBytes, &settings) + if err != nil { + log.Errorf("Could not parse global ui settings: %v\n", err) + // TODO if settings is corrupted, we probably want to alert the UI. + return &settings //firstTime = true + } + + log.Debugf("Settings: %#v", settings) + return &settings +} + +func WriteGlobalSettings(globalSettings GlobalSettings) { + bytes, _ := json.Marshal(globalSettings) + // override first time setting + globalSettings.FirstTime = true + err := GlobalSettingsFile.Write(bytes) + if err != nil { + log.Errorf("Could not write global ui settings: %v\n", err) + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..8561ad0 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,29 @@ +package utils + +import ( + "encoding/base32" + "encoding/hex" + "git.openprivacy.ca/openprivacy/log" + "strings" +) + +// temporary until we do real picture selection +func RandomProfileImage(onion string) string { + choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"} + barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) + if err != nil || len(barr) != 35 { + log.Errorf("error: %v %v %v\n", onion, err, barr) + return "extra/openprivacy.png" + } + return "profiles/" + choices[int(barr[33])%len(choices)] + ".png" +} + +func RandomGroupImage(handle string) string { + choices := []string{"001-borobudur", "002-opera-house", "003-burj-al-arab", "004-chrysler", "005-acropolis", "006-empire-state-building", "007-temple", "008-indonesia-1", "009-new-zealand", "010-notre-dame", "011-space-needle", "012-seoul", "013-mosque", "014-milan", "015-statue", "016-pyramid", "017-cologne", "018-brandenburg-gate", "019-berlin-cathedral", "020-hungarian-parliament", "021-buckingham", "022-thailand", "023-independence", "024-angkor-wat", "025-vaticano", "026-christ-the-redeemer", "027-colosseum", "028-golden-gate-bridge", "029-sphinx", "030-statue-of-liberty", "031-cradle-of-humankind", "032-istanbul", "033-london-eye", "034-sagrada-familia", "035-tower-bridge", "036-burj-khalifa", "037-washington", "038-big-ben", "039-stonehenge", "040-white-house", "041-ahu-tongariki", "042-capitol", "043-eiffel-tower", "044-church-of-the-savior-on-spilled-blood", "045-arc-de-triomphe", "046-windmill", "047-louvre", "048-torii-gate", "049-petronas", "050-matsumoto-castle", "051-fuji", "052-temple-of-heaven", "053-pagoda", "054-chichen-itza", "055-forbidden-city", "056-merlion", "057-great-wall-of-china", "058-taj-mahal", "059-pisa", "060-indonesia"} + barr, err := hex.DecodeString(handle) + if err != nil || len(barr) == 0 { + log.Errorf("error: %v %v %v\n", handle, err, barr) + return "extra/openprivacy.png" + } + return "servers/" + choices[int(barr[0])%len(choices)] + ".png" +}