commit 0ecda3d3d55e0d96b24d2f5594046f92c6c470ba Author: Sarah Jamie Lewis Date: Tue Feb 21 12:31:49 2023 -0800 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..076c93d --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea/ +lib.go +templates/bindings.go +templates/imports.go +cwtch-sources.jar +cwtch.aar +libCwtch.h +libCwtch.so +libCwtch.dylib +libCwtch.dll +ios/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3f02a70 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +IOS_OUT := ./ios + +.PHONY: all linux android windows macos clean ios + +DEFAULT_GOAL: linux + +all: linux android windows + +linux: libCwtch.so + +macos: libCwtch.x64.dylib libCwtch.arm64.dylib + +android: cwtch.aar + +windows: libCwtch.dll + +libCwtch.so: lib.go + ./switch-ffi.sh + go build -trimpath -ldflags "-buildid=autobindings-$(shell git describe --tags) -X main.buildVer=autobindings-$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.so + +libCwtch.x64.dylib: lib.go + ./switch-ffi.sh + go build -trimpath -ldflags "-buildid=autobindings-$(shell git describe --tags) -X main.buildVer=autobindings-$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.x64.dylib + +libCwtch.arm64.dylib: lib.go + ./switch-ffi.sh + env GOARCH=arm64 GOOS=darwin CGO_ENABLED=1 go build -trimpath -ldflags "-buildid=$(shell git describe --tags) -X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" -buildmode c-shared -o libCwtch.arm64.dylib + +cwtch.aar: lib.go + ./switch-gomobile.sh + gomobile bind -trimpath -target android/arm,android/arm64,android/amd64 -ldflags="-buildid=$(shell git describe --tags) -X cwtch.buildVer=$(shell git describe --tags) -X cwtch.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)" + +libCwtch.dll: lib.go + ./switch-ffi.sh + # '-Xlinker --no-insert-timestamp` sets the output dll PE timestamp header to all zeros, instead of the actual time + # this is necessary for reproducible builds (see: https://wiki.debian.org/ReproducibleBuilds/TimestampsInPEBinaries for additional information) + # note: the above documentation also references an ability to set an optional timestamp - this behaviour seems to no longer be supported in more recent versions of mingw32-gcc (the help docs no longer reference that functionality) + # these flags have to be passed through to the underlying gcc process using the -extldflags option in the underlying go linker, note that the whole flag is quoted...this is necessary. + GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-win32 go build -trimpath -ldflags "-buildid=$(shell git describe --tags) -X main.buildVer=$(shell git describe --tags) -X main.buildDate=$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M) '-extldflags=-Xlinker --no-insert-timestamp'" -buildmode c-shared -o libCwtch.dll + +clean: + rm -f cwtch.aar cwtch_go.apk libCwtch.h libCwtch.so cwtch-sources.jar libCwtch.dll libCwtch.dylib + +# iOS - for testing purposes only for now, not officially supported + +ios-arm64: + CGO_ENABLED=1 \ + GOOS=darwin \ + GOARCH=arm64 \ + SDK=iphoneos \ + CGO_CFLAGS="-fembed-bitcode" \ + CC=$(PWD)/clangwrap.sh \ + go build -buildmode=c-archive -tags ios -o $(IOS_OUT)/arm64.a . + +ios-x86_64: + CGO_ENABLED=1 \ + GOOS=darwin \ + GOARCH=amd64 \ + SDK=iphonesimulator \ + CC=$(PWD)/clangwrap.sh \ + go build -buildmode=c-archive -tags ios -o $(IOS_OUT)/x86_64.a . + +ios: ios-arm64 ios-x86_64 + lipo $(IOS_OUT)/x86_64.a $(IOS_OUT)/arm64.a -create -output $(IOS_OUT)/cwtch.a + cp $(IOS_OUT)/arm64.h $(IOS_OUT)/cwtch.h diff --git a/acn.go b/acn.go new file mode 100644 index 0000000..50ada0a --- /dev/null +++ b/acn.go @@ -0,0 +1,103 @@ +package main + +import ( + "crypto/rand" + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/event" + "encoding/base64" + "fmt" + "git.openprivacy.ca/openprivacy/connectivity" + "git.openprivacy.ca/openprivacy/connectivity/tor" + "git.openprivacy.ca/openprivacy/log" + mrand "math/rand" + "os" + path "path/filepath" + "strings" + "time" +) + +const ( + CwtchStarted = event.Type("CwtchStarted") + CwtchStartError = event.Type("CwtchStartError") + UpdateGlobalSettings = event.Type("UpdateGlobalSettings") +) + +func buildACN(settings app.GlobalSettings, torPath string, appDir string) (connectivity.ACN, app.GlobalSettings) { + + mrand.Seed(int64(time.Now().Nanosecond())) + socksPort := mrand.Intn(1000) + 9600 + controlPort := socksPort + 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("making directory %v", appDir) + err = os.MkdirAll(path.Join(appDir, "tor"), 0700) + + if err != nil { + log.Errorf("error creating tor data directory: %v. Aborting app start up", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return &connectivity.ErrorACN{}, settings + } + + if settings.AllowAdvancedTorConfig { + controlPort = settings.CustomControlPort + socksPort = settings.CustomSocksPort + } + + torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)) + // torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice) + if settings.UseCustomTorrc { + customTorrc := settings.CustomTorrc + torrc.WithCustom(strings.Split(customTorrc, "\n")) + } else { + // Fallback to showing the freshly generated torrc for this session. + settings.CustomTorrc = torrc.Preview() + settings.CustomControlPort = controlPort + settings.CustomSocksPort = socksPort + } + + err = torrc.Build(path.Join(appDir, "tor", "torrc")) + + if err != nil { + log.Errorf("error constructing torrc: %v", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return &connectivity.ErrorACN{}, settings + } + + dataDir := settings.TorCacheDir + if !settings.UseTorCache { + + // purge data dir directories if we are not using them for a cache + torDir := path.Join(appDir, "tor") + files, err := path.Glob(path.Join(torDir, "data-dir-*")) + if err != nil { + log.Errorf("could not construct filesystem glob: %v", err) + } + for _, f := range files { + if err := os.RemoveAll(f); err != nil { + log.Errorf("could not remove data-dir: %v", err) + } + } + + if dataDir, err = os.MkdirTemp(torDir, "data-dir-"); err != nil { + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return &connectivity.ErrorACN{}, settings + } + } + + // Persist Current Data Dir as Tor Cache... + settings.TorCacheDir = dataDir + + acn, err := tor.NewTorACNWithAuth(appDir, torPath, dataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) + if err != nil { + log.Errorf("Error connecting to Tor replacing with ErrorACN: %v\n", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + acn = &connectivity.ErrorACN{} + } + return acn, settings +} diff --git a/attributes.go b/attributes.go new file mode 100644 index 0000000..f3bdaa7 --- /dev/null +++ b/attributes.go @@ -0,0 +1,84 @@ +package main + +import "C" +import ( + "cwtch.im/cwtch/model/attr" + "encoding/json" +) + +// TODO: At some point these functions should also be autogenerated + +// Attribute is a struct to return the dual values of an attempt at a Get*Attribute API call, meant to be json serialized +type Attribute struct { + Exists bool + Value string +} + +//export c_GetProfileAttribute +func c_GetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int) *C.char { + profileOnion := C.GoStringN(profile_ptr, profile_len) + key := C.GoStringN(key_ptr, key_len) + return C.CString(GetProfileAttribute(profileOnion, key)) +} + +// GetProfileAttribute provides a wrapper around profile.GetScopedZonedAttribute +// Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted. +// Currently forcing the Public Scope +// Returns json of Attribute +func GetProfileAttribute(profileOnion string, key string) string { + profile := application.GetPeer(profileOnion) + if profile != nil { + zone, key := attr.ParseZone(key) + + res, exists := profile.GetScopedZonedAttribute(attr.PublicScope, zone, key) + attr := Attribute{exists, res} + json, _ := json.Marshal(attr) + return string(json) + + } + empty := Attribute{false, ""} + json, _ := json.Marshal(empty) + return (string(json)) +} + +//export c_SetConversationAttribute +func c_SetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id 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) + key := C.GoStringN(key_ptr, key_len) + value := C.GoStringN(val_ptr, val_len) + SetConversationAttribute(profileOnion, int(conversation_id), key, value) +} + +// SetConversationAttribute provides a wrapper around profile.SetProfileAttribute +// key is of format Zone.Key, and the API forces the Local Scope +func SetConversationAttribute(profileOnion string, conversationID int, key string, value string) { + profile := application.GetPeer(profileOnion) + zone, key := attr.ParseZone(key) + profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)), value) +} + +//export c_GetConversationAttribute +func c_GetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int) *C.char { + profileOnion := C.GoStringN(profile_ptr, profile_len) + key := C.GoStringN(key_ptr, key_len) + return C.CString(GetConversationAttribute(profileOnion, int(conversation_id), key)) +} + +// GetGonversationAttribute provides a wrapper around profile.GetGonversationAttribute +// key is of format Scope.Zone.Key +// Returns json of an Attribute +func GetConversationAttribute(profileOnion string, conversationID int, key string) string { + profile := application.GetPeer(profileOnion) + if profile != nil { + scope, zonekey := attr.ParseScope(key) + zone, key := attr.ParseZone(zonekey) + + res, err := profile.GetConversationAttribute(conversationID, scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))) + attr := Attribute{err == nil, res} + json, _ := json.Marshal(attr) + return string(json) + } + empty := Attribute{false, ""} + json, _ := json.Marshal(empty) + return (string(json)) +} diff --git a/constants/attributes.go b/constants/attributes.go new file mode 100644 index 0000000..a50d7b5 --- /dev/null +++ b/constants/attributes.go @@ -0,0 +1,37 @@ +package constants + +const SchemaVersion = "schemaVersion" + +const Name = "name" +const LastRead = "last-read" +const Picture = "picture" +const DefaultProfilePicture = "defaultPicture" +const ShowBlocked = "show-blocked" +const Archived = "archived" +const LastSeenTime = "lastMessageSeenTime" + +const ProfileTypeV1DefaultPassword = "v1-defaultPassword" +const ProfileTypeV1Password = "v1-userPassword" + +// PeerOnline stores state on if the peer believes it is online +const PeerOnline = "peer-online" + +const PeerAutostart = "autostart" + +// Description is used on server contacts, +const Description = "description" + +// ConversationNotificationPolicy is the attribute label for conversations. When App NotificationPolicy is OptIn a true value here opts in +const ConversationNotificationPolicy = "notification-policy" + +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" + +// App Experiments +const MessageFormattingExperiment = "message-formatting" diff --git a/constants/globals.go b/constants/globals.go new file mode 100644 index 0000000..ed300fd --- /dev/null +++ b/constants/globals.go @@ -0,0 +1,35 @@ +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 +// https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html +const DefactoPasswordForUnencryptedProfiles = "be gay do crime" + +const ( + // StatusSuccess is an event response for event.Status signifying a call succeeded + StatusSuccess = "success" + // StatusError is an event response for event.Status signifying a call failed in error, ideally accompanied by a event.Error + StatusError = "error" +) + +type NotificationType string + +const ( + // NotificationNone enum for message["notification"] that means no notification + NotificationNone = NotificationType("None") + // NotificationEvent enum for message["notification"] that means emit a notification that a message event happened only + NotificationEvent = NotificationType("SimpleEvent") + // NotificationConversation enum for message["notification"] that means emit a notification event with Conversation handle included + NotificationConversation = NotificationType("ContactInfo") +) + +const ( + // ConversationNotificationPolicyDefault enum for conversations indicating to use global notification policy + ConversationNotificationPolicyDefault = "ConversationNotificationPolicy.Default" + // ConversationNotificationPolicyOptIn enum for conversation indicating to opt in to nofitications when allowed + ConversationNotificationPolicyOptIn = "ConversationNotificationPolicy.OptIn" + // ConversationNotificationPolicyNever enum for conversation indicating to opt in to never do notifications + ConversationNotificationPolicyNever = "ConversationNotificationPolicy.Never" +) + +const DartIso8601 = "2006-01-02T15:04:05.999Z" diff --git a/features/groups/group_functionality.go b/features/groups/group_functionality.go new file mode 100644 index 0000000..db31cb0 --- /dev/null +++ b/features/groups/group_functionality.go @@ -0,0 +1,66 @@ +package groups + +import ( + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + constants2 "cwtch.im/cwtch/model/constants" + "cwtch.im/cwtch/peer" + "cwtch.im/cwtch/protocol/connections" + "fmt" + "git.openprivacy.ca/cwtch.im/libcwtch-go/constants" +) + +const groupExperiment = "tapir-groups-experiment" + +const ( + // ServerList is a json encoded list of servers + ServerList = event.Field("ServerList") +) + +const ( + // UpdateServerInfo is an event containing a ProfileOnion and a ServerList + UpdateServerInfo = event.Type("UpdateServerInfo") +) + +// 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) +} + +// GetServerInfoList compiles all the information the UI might need regarding all servers.. +func (gf *GroupFunctionality) GetServerInfoList(profile peer.CwtchPeer) []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.CwtchPeer) Server { + serverInfo, _ := profile.FetchConversationInfo(serverOnion) + keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass} + var serverKeys []ServerKey + + for _, keyType := range keyTypes { + if key, has := serverInfo.GetAttribute(attr.PublicScope, attr.ServerKeyZone, string(keyType)); has { + serverKeys = append(serverKeys, ServerKey{Type: string(keyType), Key: key}) + } + } + + description, _ := serverInfo.GetAttribute(attr.LocalScope, attr.ServerZone, constants.Description) + startTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants2.SyncPreLastMessageTime)).ToString()] + recentTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants2.SyncMostRecentMessageTime)).ToString()] + syncStatus := SyncStatus{startTimeStr, recentTimeStr} + + return Server{Onion: serverOnion, Identifier: serverInfo.ID, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys, Description: description, SyncProgress: syncStatus} +} diff --git a/features/groups/group_functionality_test.go b/features/groups/group_functionality_test.go new file mode 100644 index 0000000..84fa070 --- /dev/null +++ b/features/groups/group_functionality_test.go @@ -0,0 +1,23 @@ +package groups + +import "testing" + +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..f48592c --- /dev/null +++ b/features/groups/server.go @@ -0,0 +1,20 @@ +package groups + +type ServerKey struct { + Type string `json:"type"` + Key string `json:"key"` +} + +type SyncStatus struct { + StartTime string `json:"startTime"` + LastMessageTime string `json:"lastMessageTime"` +} + +type Server struct { + Onion string `json:"onion"` + Identifier int `json:"identifier"` + Status string `json:"status"` + Description string `json:"description"` + Keys []ServerKey `json:"keys"` + SyncProgress SyncStatus `json:"syncProgress"` +} 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/features/servers/servers_functionality.go b/features/servers/servers_functionality.go new file mode 100644 index 0000000..6b25c1a --- /dev/null +++ b/features/servers/servers_functionality.go @@ -0,0 +1,220 @@ +package servers + +import ( + "cwtch.im/cwtch/event" + "fmt" + "git.openprivacy.ca/cwtch.im/server" + "git.openprivacy.ca/openprivacy/connectivity" + "git.openprivacy.ca/openprivacy/log" + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +const serversExperiment = "servers-experiment" + +const ( + ZeroServersLoaded = event.Type("ZeroServersLoaded") + NewServer = event.Type("NewServer") + ServerIntentUpdate = event.Type("ServerIntentUpdate") + ServerDeleted = event.Type("ServerDeleted") + ServerStatsUpdate = event.Type("ServerStatsUpdate") +) + +const ( + Intent = event.Field("Intent") + TotalMessages = event.Field("TotalMessages") + Connections = event.Field("Connections") +) + +const ( + IntentRunning = "running" + IntentStopped = "stopped" +) + +// TODO: move into Cwtch model/attr + +type ServerInfo struct { + Onion string + ServerBundle string + Autostart bool + Running bool + Description string + StorageType string +} + +type PublishFn func(event.Event) + +var lock sync.Mutex +var appServers server.Servers +var publishFn PublishFn +var killStatsUpdate chan bool = make(chan bool, 1) +var enabled bool = false + +func InitServers(acn connectivity.ACN, appdir string, pfn PublishFn) { + lock.Lock() + defer lock.Unlock() + if appServers == nil { + serversDir := filepath.Join(appdir, "servers") + err := os.MkdirAll(serversDir, 0700) + if err != nil { + log.Errorf("Could not init servers directory: %s", err) + } + appServers = server.NewServers(acn, serversDir) + publishFn = pfn + } +} + +func Disable() { + lock.Lock() + defer lock.Unlock() + if appServers != nil { + appServers.Stop() + } + if enabled { + enabled = false + killStatsUpdate <- true + } +} + +func Enabled() bool { + lock.Lock() + defer lock.Unlock() + return enabled +} + +// ServersFunctionality provides experiment gated server functionality +type ServersFunctionality struct { +} + +// ExperimentGate returns ServersFunctionality if the experiment is enabled, and an error otherwise. +func ExperimentGate(experimentMap map[string]bool) (*ServersFunctionality, error) { + if experimentMap[serversExperiment] { + lock.Lock() + defer lock.Unlock() + return &ServersFunctionality{}, nil + } + return nil, fmt.Errorf("gated by %v", serversExperiment) +} + +func (sf *ServersFunctionality) Enable() { + lock.Lock() + defer lock.Unlock() + if appServers != nil && !enabled { + enabled = true + go cacheForwardServerMetricUpdates() + } +} + +func (sf *ServersFunctionality) LoadServers(password string) ([]string, error) { + servers, err := appServers.LoadServers(password) + // server:1.3/libcwtch-go:1.4 accidentally enabled monitor logging by default. make sure it's turned off + for _, onion := range servers { + server := appServers.GetServer(onion) + server.SetMonitorLogging(false) + } + return servers, err +} + +func (sf *ServersFunctionality) CreateServer(password string) (server.Server, error) { + return appServers.CreateServer(password) +} + +func (sf *ServersFunctionality) GetServer(onion string) server.Server { + return appServers.GetServer(onion) +} + +func (sf *ServersFunctionality) GetServerStatistics(onion string) server.Statistics { + s := appServers.GetServer(onion) + if s != nil { + return s.GetStatistics() + } + return server.Statistics{} +} + +func (sf *ServersFunctionality) ListServers() []string { + return appServers.ListServers() +} + +func (sf *ServersFunctionality) DeleteServer(onion string, currentPassword string) error { + return appServers.DeleteServer(onion, currentPassword) +} + +func (sf *ServersFunctionality) LaunchServer(onion string) { + appServers.LaunchServer(onion) + server := appServers.GetServer(onion) + if server != nil { + newStats := server.GetStatistics() + publishFn(event.NewEventList(ServerStatsUpdate, event.Identity, onion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections))) + } +} + +func (sf *ServersFunctionality) StopServer(onion string) { + appServers.StopServer(onion) +} + +func (sf *ServersFunctionality) DestroyServers() { + appServers.Destroy() +} + +func (sf *ServersFunctionality) GetServerInfo(onion string) *ServerInfo { + s := sf.GetServer(onion) + var serverInfo ServerInfo + serverInfo.Onion = s.Onion() + serverInfo.ServerBundle = s.ServerBundle() + serverInfo.Autostart = s.GetAttribute(server.AttrAutostart) == "true" + running, _ := s.CheckStatus() + serverInfo.Running = running + serverInfo.Description = s.GetAttribute(server.AttrDescription) + serverInfo.StorageType = s.GetAttribute(server.AttrStorageType) + return &serverInfo +} + +func (si *ServerInfo) EnrichEvent(e *event.Event) { + e.Data["Onion"] = si.Onion + e.Data["ServerBundle"] = si.ServerBundle + e.Data["Description"] = si.Description + e.Data["StorageType"] = si.StorageType + if si.Autostart { + e.Data["Autostart"] = "true" + } else { + e.Data["Autostart"] = "false" + } + if si.Running { + e.Data["Running"] = "true" + } else { + e.Data["Running"] = "false" + } +} + +// cacheForwardServerMetricUpdates every minute gets metrics for all servers, and if they have changed, sends events to the UI +func cacheForwardServerMetricUpdates() { + var cache map[string]server.Statistics = make(map[string]server.Statistics) + duration := time.Second // allow first load + for { + select { + case <-time.After(duration): + duration = time.Minute + serverList := appServers.ListServers() + for _, serverOnion := range serverList { + server := appServers.GetServer(serverOnion) + if running, err := server.CheckStatus(); running && err == nil { + newStats := server.GetStatistics() + if stats, ok := cache[serverOnion]; !ok || stats.TotalConnections != newStats.TotalConnections || stats.TotalMessages != newStats.TotalMessages { + cache[serverOnion] = newStats + publishFn(event.NewEventList(ServerStatsUpdate, event.Identity, serverOnion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections))) + } + } + } + case <-killStatsUpdate: + return + } + } +} + +func Shutdown() { + Disable() + appServers.Destroy() +} diff --git a/generate/generate_bindings.go b/generate/generate_bindings.go new file mode 100644 index 0000000..ed0c7c1 --- /dev/null +++ b/generate/generate_bindings.go @@ -0,0 +1,373 @@ +package main + +import "C" +import ( + "bufio" + "fmt" + "log" + "os" + "regexp" + "strings" +) + +func main() { + + generatedBindingsPrefix := `` + generatedBindings := `` + file, err := os.Open("spec") + if err != nil { + log.Fatal(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + // optionally, resize scanner's capacity for lines over 64K, see next example + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "#") || len(line) == 0 { + // ignore + continue + } + + parts := strings.Split(line, " ") + if len(parts) < 2 { + fmt.Printf("all spec lines must start with a type prefix and a function name: %v\n", parts) + os.Exit(1) + } + + fType := parts[0] + fName := parts[1] + + if strings.HasPrefix(line, "import") { + generatedBindingsPrefix += fName + "\n" + continue + } + + fmt.Printf("generating %v function for %v\n", fType, fName) + + switch fType { + + case "app": + generatedBindings = generateAppFunction(generatedBindings, fName, parts[2:]) + case "profile": + generatedBindings = generateProfileFunction(generatedBindings, fName, parts[2:]) + case "(json)profile": + generatedBindings = generateJsonProfileFunction(generatedBindings, fName, parts[2:]) + case "@profile-experiment": + experiment := parts[2] + generatedBindings = generateExperimentalProfileFunction(generatedBindings, experiment, fName, parts[3:]) + case "@(json)profile-experiment": + experiment := parts[2] + generatedBindings = generateExperimentalJsonProfileFunction(generatedBindings, experiment, fName, parts[3:]) + default: + fmt.Printf("unknown function type %v\n", parts) + os.Exit(1) + } + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + fmt.Printf("%v\n", generatedBindings) + os.WriteFile("templates/bindings.go", []byte(generatedBindings), 0644) + os.WriteFile("templates/imports.go", []byte(generatedBindingsPrefix), 0644) + + template, _ := os.ReadFile("templates/lib_template.go") + templateString := string(template) + templateString = strings.ReplaceAll(templateString, "{{BINDINGS}}", generatedBindings) + templateString = strings.ReplaceAll(templateString, "{{IMPORTS}}", generatedBindingsPrefix) + os.WriteFile("lib.go", []byte(templateString), 0644) +} + +var uniqueVarCounter = 0 + +func profileHandleArgPrototype() (string, string, string, string) { + return `onion_ptr *C.char, onion_len C.int`, `C.GoStringN(onion_ptr, onion_len)`, `profile string`, "profile" +} + +func nameArgPrototype() (string, string, string, string) { + return `name_ptr *C.char, name_len C.int`, `C.GoStringN(name_ptr, name_len)`, `name string`, "name" +} + +func passwordArgPrototype() (string, string, string, string) { + return `password_ptr *C.char, password_len C.int`, `C.GoStringN(password_ptr, password_len)`, `password string`, "password" +} + +func intArgPrototype(varName string) (string, string, string, string) { + return fmt.Sprintf(`%s C.int`, ToSnakeCase(varName)), fmt.Sprintf(`int(%v)`, ToSnakeCase(varName)), fmt.Sprintf(`%v int`, varName), varName +} + +func conversationArgPrototype(varName string) (string, string, string, string) { + return intArgPrototype(varName) +} + +func channelArgPrototype() (string, string, string, string) { + return intArgPrototype("channel_id") +} + +func messageArgPrototype() (string, string, string, string) { + return intArgPrototype("message_id") +} + +func boolArgPrototype(name string) (string, string, string, string) { + uniqueVarCounter += 1 + varName := fmt.Sprintf("%s%d", name, uniqueVarCounter) + return fmt.Sprintf(`%s C.char`, ToSnakeCase(varName)), fmt.Sprintf(`%v == 1`, ToSnakeCase(varName)), fmt.Sprintf(`%v bool`, varName), varName +} + +func stringArgPrototype(name string) (string, string, string, string) { + uniqueVarCounter += 1 + varName := fmt.Sprintf("%s%d", name, uniqueVarCounter) + return fmt.Sprintf(`%s_ptr *C.char, %s_len C.int`, varName, varName), fmt.Sprintf(`C.GoStringN(%s_ptr, %s_len)`, varName, varName), fmt.Sprintf(`%v string`, varName), varName +} + +func mapArgs(argsTypes []string) (string, string, string, string) { + var cArgs []string + var c2GoArgs []string + var goSpec []string + var gUse []string + for _, argSpec := range argsTypes { + argTypeParts := strings.Split(argSpec, ":") + argType := argTypeParts[0] + switch argType { + case "profile": + c1, c2, c3, c4 := profileHandleArgPrototype() + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "name": + c1, c2, c3, c4 := nameArgPrototype() + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "password": + c1, c2, c3, c4 := passwordArgPrototype() + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "conversation": + name := "conversation" + if len(argTypeParts) == 2 { + name = argTypeParts[1] + } + c1, c2, c3, c4 := conversationArgPrototype(name) + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "channel": + c1, c2, c3, c4 := channelArgPrototype() + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "message": + c1, c2, c3, c4 := messageArgPrototype() + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "bool": + if len(argTypeParts) != 2 { + fmt.Printf("generic bool arg must have have e.g. bool:\n") + os.Exit(1) + } + c1, c2, c3, c4 := boolArgPrototype(argTypeParts[1]) + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "int": + if len(argTypeParts) != 2 { + fmt.Printf("generic bool arg must have have e.g. bool:\n") + os.Exit(1) + } + c1, c2, c3, c4 := intArgPrototype(argTypeParts[1]) + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + case "string": + if len(argTypeParts) != 2 { + fmt.Printf("generic string arg must have have e.g. string:\n") + os.Exit(1) + } + c1, c2, c3, c4 := stringArgPrototype(argTypeParts[1]) + cArgs = append(cArgs, c1) + c2GoArgs = append(c2GoArgs, c2) + goSpec = append(goSpec, c3) + gUse = append(gUse, c4) + default: + fmt.Printf("unknown arg type [%v]\n", argType) + os.Exit(1) + } + } + return strings.Join(cArgs, ","), strings.Join(c2GoArgs, ","), strings.Join(goSpec, ","), strings.Join(gUse, ",") +} + +func generateAppFunction(bindings string, name string, argsTypes []string) string { + appPrototype := ` +//export c_{{FNAME}} +func c_{{FNAME}}({{C_ARGS}}) { + {{FNAME}}({{C2GO_ARGS}}) +} + +func {{FNAME}}({{GO_ARGS_SPEC}}) { + application.{{LIBNAME}}({{GO_ARG}}) +} +` + + cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) + appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) + appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) + appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs) + appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) + + bindings += appPrototype + return bindings +} + +func generateProfileFunction(bindings string, name string, argsTypes []string) string { + appPrototype := ` +//export c_{{FNAME}} +func c_{{FNAME}}({{C_ARGS}}) { + {{FNAME}}({{C2GO_ARGS}}) +} + +func {{FNAME}}({{GO_ARGS_SPEC}}) { + cwtchProfile := application.GetPeer(profile) + if cwtchProfile != nil { + cwtchProfile.{{LIBNAME}}({{GO_ARG}}) + } +} +` + + cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) + + appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) + appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) + // We need to prepend a set of profile handle arguments... + pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() + appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) + + bindings += appPrototype + return bindings +} + +func generateJsonProfileFunction(bindings string, name string, argsTypes []string) string { + appPrototype := ` +//export c_{{FNAME}} +func c_{{FNAME}}({{C_ARGS}}) *C.char { + return C.CString({{FNAME}}({{C2GO_ARGS}})) +} + +func {{FNAME}}({{GO_ARGS_SPEC}}) string { + cwtchProfile := application.GetPeer(profile) + if cwtchProfile != nil { + return cwtchProfile.{{LIBNAME}}({{GO_ARG}}) + } + return "" +} +` + + cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) + + appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) + appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) + // We need to prepend a set of profile handle arguments... + pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() + appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) + + bindings += appPrototype + return bindings +} + +func generateExperimentalProfileFunction(bindings string, experiment string, name string, argsTypes []string) string { + appPrototype := ` +//export c_{{FNAME}} +func c_{{FNAME}}({{C_ARGS}}) { + {{FNAME}}({{C2GO_ARGS}}) +} + +func {{FNAME}}({{GO_ARGS_SPEC}}) { + cwtchProfile := application.GetPeer(profile) + if cwtchProfile != nil { + functionality, _ := {{EXPERIMENT}}.FunctionalityGate(application.ReadSettings().Experiments) + if functionality != nil { + functionality.{{LIBNAME}}(cwtchProfile, {{GO_ARG}}) + } + } +} +` + + cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) + + appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) + appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) + appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", experiment) + // We need to prepend a set of profile handle arguments... + pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() + appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) + + bindings += appPrototype + return bindings +} + +func generateExperimentalJsonProfileFunction(bindings string, experiment string, name string, argsTypes []string) string { + appPrototype := ` +//export c_{{FNAME}} +func c_{{FNAME}}({{C_ARGS}}) *C.char { + return C.CString({{FNAME}}({{C2GO_ARGS}})) +} + +func {{FNAME}}({{GO_ARGS_SPEC}}) string { + cwtchProfile := application.GetPeer(profile) + if cwtchProfile != nil { + functionality, _ := {{EXPERIMENT}}.FunctionalityGate(application.ReadSettings().Experiments) + if functionality != nil { + return functionality.{{LIBNAME}}(cwtchProfile, {{GO_ARG}}) + } + } + return "" +} +` + + cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) + + appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) + appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) + appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", experiment) + // We need to prepend a set of profile handle arguments... + pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() + appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) + + bindings += appPrototype + return bindings +} + +var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +func ToSnakeCase(str string) string { + snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + return strings.ToLower(snake) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7fa259b --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module git.openprivacy.ca/cwtch.im/cwtch-autobindings + +go 1.19 + +require ( + cwtch.im/cwtch v0.18.10 + git.openprivacy.ca/cwtch.im/libcwtch-go v1.10.5 + git.openprivacy.ca/cwtch.im/server v1.4.5 + git.openprivacy.ca/openprivacy/connectivity v1.8.6 + git.openprivacy.ca/openprivacy/log v1.0.3 + github.com/mutecomm/go-sqlcipher/v4 v4.4.2 +) + +replace cwtch.im/cwtch => /home/sarah/workspace/src/cwtch.im/cwtch + +require ( + filippo.io/edwards25519 v1.0.0 // indirect + git.openprivacy.ca/cwtch.im/tapir v0.6.0 // indirect + git.openprivacy.ca/openprivacy/bine v0.0.4 // indirect + github.com/gtank/merlin v0.1.1 // indirect + github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c // indirect + github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect + go.etcd.io/bbolt v1.3.6 // indirect + golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect + golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect + golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..93e8b1a --- /dev/null +++ b/go.sum @@ -0,0 +1,170 @@ +cwtch.im/cwtch v0.18.0/go.mod h1:StheazFFY7PKqBbEyDVLhzWW6WOat41zV0ckC240c5Y= +cwtch.im/cwtch v0.18.10 h1:iTzLzlms1mgn8kLfClU/yAWIVWVRRT8UmfbDNli9dzE= +cwtch.im/cwtch v0.18.10/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +git.openprivacy.ca/cwtch.im/libcwtch-go v1.10.5 h1:xqLna6aNK5lj08hkspmK3VWVOyOMEnwAfcWQpR8ZCm0= +git.openprivacy.ca/cwtch.im/libcwtch-go v1.10.5/go.mod h1:FTW3OcWTy5oQYCUe8ACz0D2jAzCembDyl8FfATjsVmM= +git.openprivacy.ca/cwtch.im/server v1.4.5 h1:QuNAIxld+aWeQfWuGHB2QYZXsqJMmTyl55Pcmdn8FQA= +git.openprivacy.ca/cwtch.im/server v1.4.5/go.mod h1:dGB1bePUgDU9xwk7gGkioNeshrbNgGWhSH8zMQwIAUg= +git.openprivacy.ca/cwtch.im/tapir v0.5.5/go.mod h1:bWWHrDYBtHvxMri59RwIB/w7Eg1aC0BrQ/ycKlnbB5k= +git.openprivacy.ca/cwtch.im/tapir v0.6.0 h1:TtnKjxitkIDMM7Qn0n/u+mOHRLJzuQUYjYRu5n0/QFY= +git.openprivacy.ca/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY= +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.8.6 h1:g74PyDGvpMZ3+K0dXy3mlTJh+e0rcwNk0XF8owzkmOA= +git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg= +git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0= +git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +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.3-0.20210930101514-6bb39798585c h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE= +github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk= +github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU= +github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc= +github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +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/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/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-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/quality.sh b/quality.sh new file mode 100755 index 0000000..c1040e4 --- /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 vet generate/* + +echo "" +echo "Linting:" + +staticcheck ./generate + + +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 "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea" diff --git a/spec b/spec new file mode 100644 index 0000000..cfd8768 --- /dev/null +++ b/spec @@ -0,0 +1,42 @@ +# ( app | profile) FunctionName (profile) + +# Peer Engine +app ActivatePeerEngine profile +app DeactivatePeerEngine profile + +# Profile Management +app CreateProfile name password bool:autostart +app LoadProfiles password +app DeleteProfile profile password +app ImportProfile string:file password +profile ChangePassword string:current string:newPassword string:newPasswordAgain +profile ExportProfile string:file + +# Conversation Management +profile ImportBundle string:bundle +profile ArchiveConversation conversation +profile AcceptConversation conversation +profile BlockConversation conversation +profile UnblockConversation conversation +profile DeleteConversation conversation + +# Message Management +(json)profile EnhancedSendMessage conversation string:msg +(json)profile EnhancedGetMessageById conversation message +(json)profile EnhancedGetMessageByContentHash conversation string:contentHash +(json)profile EnhancedGetMessages conversation int:index int:count +# (json)profile SendInviteToConversation conversation conversation:target +profile UpdateMessageAttribute conversation channel message string:attributeKey string:attributeValue + +# Group Management +profile StartGroup string:name string:server + +# Filesharing Management +import "cwtch.im/cwtch/functionality/filesharing" +@profile-experiment DownloadFileDefaultLimit filesharing conversation string:filepath string:manifest string:filekey +@profile-experiment RestartFileShare filesharing string:filekey +@profile-experiment StopFileShare filesharing string:filekey +@profile-experiment CheckDownloadStatus filesharing string:filekey +@profile-experiment VerifyOrResumeDownload filesharing conversation string:filekey +@(json)profile-experiment EnhancedShareFile filesharing conversation string:filepath +@(json)profile-experiment EnhancedGetSharedFiles filesharing conversation diff --git a/switch-ffi.sh b/switch-ffi.sh new file mode 100755 index 0000000..221fe92 --- /dev/null +++ b/switch-ffi.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# using '-i.bak' and 'rm .bak' for gnu sed (linux) and bsd sed (macosx) compat +sed -i.bak "s/^package cwtch/\/\/package cwtch/" lib.go +sed -i.bak "s/^\/\/package main/package main/" lib.go +sed -i.bak "s/^\/\/func main()/func main()/" lib.go +rm lib.go.bak diff --git a/switch-gomobile.sh b/switch-gomobile.sh new file mode 100755 index 0000000..bd59e2b --- /dev/null +++ b/switch-gomobile.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# using '-i.bak' and 'rm .bak' for gnu sed (linux) and bsd sed (macosx) compat +sed -i.bak "s/^\/\/package cwtch/package cwtch/" lib.go +sed -i.bak "s/^package main/\/\/package main/" lib.go +sed -i.bak "s/^func main()/\/\/func main()/" lib.go +rm lib.go.bak \ No newline at end of file diff --git a/templates/lib_template.go b/templates/lib_template.go new file mode 100644 index 0000000..ee3a37d --- /dev/null +++ b/templates/lib_template.go @@ -0,0 +1,282 @@ +//package cwtch + +package main + +// //Needed to invoke C.free +// #include +import "C" + +import ( + "cwtch.im/cwtch/event" + "encoding/json" + "fmt" + "git.openprivacy.ca/cwtch.im/cwtch-autobindings/utils" + "git.openprivacy.ca/openprivacy/log" + "os" + "os/user" + path "path/filepath" + "runtime" + "strings" + "time" + "unsafe" + + // Import SQL Cipher + _ "github.com/mutecomm/go-sqlcipher/v4" + + "cwtch.im/cwtch/app" + "git.openprivacy.ca/openprivacy/connectivity" + + {{IMPORTS}} +) + +// supplied by make +var ( + buildVer string + buildDate string +) + +var application app.Application +var globalAppDir string +var globalTorPath string +var eventHandler *utils.EventHandler +var globalACN connectivity.ProxyACN + +// Dangerous function. Should only be used as documented in `MEMORY.md` +// +//export c_FreePointer +func c_FreePointer(ptr *C.char) { + C.free(unsafe.Pointer(ptr)) +} + +//export c_Started +func c_Started() C.int { + return C.int(Started()) +} + +// Started returns 1 if application is initialized and 0 if it is null +func Started() int { + if application == nil { + return 0 + } + return 1 +} + +//export c_StartCwtch +func c_StartCwtch(dir_c *C.char, len C.int, tor_c *C.char, torLen C.int) C.int { + applicationDirectory := C.GoStringN(dir_c, len) + torDirectory := C.GoStringN(tor_c, torLen) + return C.int(StartCwtch(applicationDirectory, torDirectory)) +} + +// 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 { + if logfile := os.Getenv("LOG_FILE"); logfile != "" { + filelog, err := log.NewFile(log.LevelInfo, logfile) + if err == nil { + filelog.SetUseColor(false) + log.SetStd(filelog) + } else { + // not so likely to be seen since we're usually creating file log in situations we can't access console logs... + log.Errorf("could not create file log: %v\n", err) + } + } + if runtime.GOOS == "android" { + log.SetUseColor(false) + } + log.SetLevel(log.LevelInfo) + if logLevel := os.Getenv("LOG_LEVEL"); strings.ToLower(logLevel) == "debug" { + log.SetLevel(log.LevelDebug) + } + log.Infof("StartCwtch(...)") + log.Debugf("builddate: %v buildver: %v", buildDate, buildVer) + + // 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", application, eventHandler) + + if application != nil { + log.Infof("_startCwtch detected existing application; resuming instead of relaunching") + ReconnectCwtchForeground() + return + } + + log.Infof("Creating new EventHandler()") + + eventHandler = utils.NewEventHandler() + + // Exclude Tapir wire Messages + //(We need a TRACE level) + log.ExcludeFromPattern("service.go") + + // Environment variables don't get '~' expansion so if CWTCH_DIR was set, it likely needs manual handling + usr, _ := user.Current() + homeDir := usr.HomeDir + if appDir == "~" { + appDir = homeDir + } else if strings.HasPrefix(appDir, "~/") { + appDir = path.Join(homeDir, appDir[2:]) + } + + // Ensure that the application directory exists...and then initialize settings.. + err := os.MkdirAll(appDir, 0700) + if err != nil { + log.Errorf("Error creating appDir %v: %v\n", appDir, err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error creating appDir %v: %v", appDir, err))) + return + } + + log.Infof("Loading Cwtch Directory %v and tor path: %v", appDir, torPath) + app.InitGlobalSettingsFile(appDir, app.DefactoPasswordForUnencryptedProfiles) + + log.Infof("making directory %v", appDir) + err = os.MkdirAll(path.Join(appDir, "tor"), 0700) + + if err != nil { + log.Errorf("error creating tor data directory: %v. Aborting app start up", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return + } + + // Allow the user of a custom torrc + globalAppDir = appDir + globalTorPath = torPath + settingsFile := app.InitApp(appDir) + newACN, settings := buildACN(settingsFile.ReadGlobalSettings(), globalTorPath, globalAppDir) + globalACN := connectivity.NewProxyACN(newACN) + settingsFile.WriteGlobalSettings(settings) + application = app.NewApp(&globalACN, appDir, settingsFile) + + // Subscribe to all App Events... + eventHandler.HandleApp(application) + + // Settings may have changed... + settings = settings + settingsJson, _ := json.Marshal(settings) + + // FIXME: This code exists to allow the Splash Screen test in the new UI integration tests to pass + // it doesn't actually fix the problem in theory, and we should get around to ensuring that application + // is safe to access even if shutdown is called concurrently... + if application == nil { + log.Errorf("startCwtch: primary application object has gone away. assuming application is closing.") + return + } + // Send global settings to the UI... + application.GetPrimaryBus().Publish(event.NewEvent(app.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) + log.Infof("libcwtch-go application launched") + application.GetPrimaryBus().Publish(event.NewEvent(app.CwtchStarted, map[event.Field]string{})) + application.QueryACNVersion() + application.LoadProfiles(app.DefactoPasswordForUnencryptedProfiles) +} + +// the pointer returned from this function **must** be freed using c_Free +// +//export c_GetAppBusEvent +func c_GetAppBusEvent() *C.char { + return C.CString(GetAppBusEvent()) +} + +// GetAppBusEvent blocks until an event +func GetAppBusEvent() string { + for eventHandler == nil { + log.Debugf("waiting for eventHandler != nil") + time.Sleep(time.Second) + } + + var json = "" + for json == "" { + json = eventHandler.GetNextEvent() + } + return json +} + +//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 cwtch foreground") + if application == nil { + log.Errorf("ReconnectCwtchForeground: Application is nil, presuming stale thread, EXITING Reconnect\n") + return + } + + // TODO: Need To Repopulate UI + + settingsJson, _ := json.Marshal(application.ReadSettings()) + application.GetPrimaryBus().Publish(event.NewEvent(app.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) + application.GetPrimaryBus().Publish(event.NewEvent(app.CwtchStarted, map[event.Field]string{})) + application.QueryACNStatus() + application.QueryACNVersion() +} + +//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 { + // 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 + eventHandler = nil + } +} + +//export c_ResetTor +func c_ResetTor() { + ResetTor() +} + +func ResetTor() { + log.Infof("Replacing ACN with new Tor...") + settings := application.ReadSettings() + + globalACN.Close() // we need to close first if dateDir is the same, otherwise buildACN can't launch tor. + newAcn, settings := buildACN(settings, globalTorPath, globalAppDir) + application.UpdateSettings(settings) + globalACN.ReplaceACN(newAcn) + application.QueryACNVersion() + + // We need to update settings on reset as buildACN can alter settings, otherwise the next reset will be broken... + settings = application.ReadSettings() + settingsJson, _ := json.Marshal(settings) + application.GetPrimaryBus().Publish(event.NewEvent(UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)})) + log.Infof("Restarted") +} + +{{BINDINGS}} + +// Leave as is, needed by ffi +func main() {} diff --git a/utils/contacts.go b/utils/contacts.go new file mode 100644 index 0000000..1fbb554 --- /dev/null +++ b/utils/contacts.go @@ -0,0 +1,25 @@ +package utils + +import "cwtch.im/cwtch/model" + +type Contact struct { + Name string `json:"name"` + Onion string `json:"onion"` + Status string `json:"status"` + Picture string `json:"picture"` + DefaultPicture string `json:"defaultPicture"` + Accepted bool `json:"accepted"` + AccessControlList model.AccessControlList `json:"accessControlList"` + Blocked bool `json:"blocked"` + SaveHistory string `json:"saveConversationHistory"` + Messages int `json:"numMessages"` + Unread int `json:"numUnread"` + LastSeenMessageId int `json:"lastSeenMessageId"` + LastMessage string `json:"lastMsgTime"` + IsGroup bool `json:"isGroup"` + GroupServer string `json:"groupServer"` + IsArchived bool `json:"isArchived"` + Identifier int `json:"identifier"` + NotificationPolicy string `json:"notificationPolicy"` + Attributes map[string]string `json:"attributes"` +} diff --git a/utils/eventHandler.go b/utils/eventHandler.go new file mode 100644 index 0000000..5607a5e --- /dev/null +++ b/utils/eventHandler.go @@ -0,0 +1,744 @@ +package utils + +import ( + "encoding/json" + "fmt" + "git.openprivacy.ca/cwtch.im/cwtch-autobindings/features/servers" + "os" + "strconv" + + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/app/plugins" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/model/constants" + "cwtch.im/cwtch/peer" + "cwtch.im/cwtch/protocol/connections" + constants2 "git.openprivacy.ca/cwtch.im/cwtch-autobindings/constants" + "git.openprivacy.ca/cwtch.im/cwtch-autobindings/features/groups" + "git.openprivacy.ca/openprivacy/log" + + "time" + + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/functionality/filesharing" +) + +type EventProfileEnvelope struct { + Event event.Event + Profile string +} + +type LCG_API_Handler struct { + LaunchServers func() + StopServers func() +} + +type EventHandler struct { + app app.Application + appBusQueue event.Queue + profileEvents chan EventProfileEnvelope +} + +// We should be reading from profile events pretty quickly, but we make this buffer fairly large... +const profileEventsBufferSize = 512 + +func NewEventHandler() *EventHandler { + eh := &EventHandler{app: nil, appBusQueue: event.NewQueue(), profileEvents: make(chan EventProfileEnvelope, profileEventsBufferSize)} + return eh +} + +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.ACNVersion, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(app.UpdateGlobalSettings, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(app.CwtchStarted, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(servers.NewServer, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(servers.ServerIntentUpdate, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(servers.ServerDeleted, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(servers.ServerStatsUpdate, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.StartingStorageMiragtion, eh.appBusQueue) + application.GetPrimaryBus().Subscribe(event.DoneStorageMigration, eh.appBusQueue) +} + +func (eh *EventHandler) GetNextEvent() string { + appChan := eh.appBusQueue.OutChan() + + select { + case e := <-appChan: + return eh.handleAppBusEvent(&e) + default: + select { + case e := <-appChan: + return eh.handleAppBusEvent(&e) + case ev := <-eh.profileEvents: + return eh.handleProfileEvent(&ev) + } + } +} + +// track acnStatus across events +var acnStatus = -1 + +// handleAppBusEvent enriches AppBus events so they are usable with out further data fetches +func (eh *EventHandler) handleAppBusEvent(e *event.Event) string { + + if eh.app != nil { + switch e.EventType { + case event.ACNStatus: + newAcnStatus, err := strconv.Atoi(e.Data[event.Progress]) + if err != nil { + break + } + if newAcnStatus == 100 { + if acnStatus != 100 { + for _, onion := range eh.app.ListProfiles() { + profile := eh.app.GetPeer(onion) + autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerAutostart) + if !exists || autostart == "true" { + eh.app.ActivatePeerEngine(onion) + } + } + //eh.api.LaunchServers() + } + } else { + if acnStatus == 100 { + // just fell offline + for _, onion := range eh.app.ListProfiles() { + eh.app.DeactivatePeerEngine(onion) + } + //eh.api.StopServers() + } + } + acnStatus = newAcnStatus + case event.NewPeer: + onion := e.Data[event.Identity] + profile := eh.app.GetPeer(e.Data[event.Identity]) + if profile == nil { + log.Errorf("NewPeer: skipping profile initialization. this should only happen when the app is rapidly opened+closed (eg during testing)") + break + } + log.Debug("New Peer Event: %v", e) + + if e.Data["Reload"] != event.True { + eh.startHandlingPeer(onion) + } + + // CwtchPeer will always set this now... + tag, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag) + e.Data[constants.Tag] = tag + + if e.Data[event.Created] == event.True { + profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants2.Picture, ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro))) + } + + profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline, event.False) + // disabeling network check for connection attempt reservation, needs rework + //eh.app.AddPeerPlugin(onion, plugins.NETWORKCHECK) + eh.app.AddPeerPlugin(onion, plugins.ANTISPAM) + + // If the user has chosen to block unknown profiles + // then explicitly configure the protocol engine to do so.. + settings := eh.app.ReadSettings() + if settings.BlockUnknownConnections { + profile.BlockUnknownConnections() + } else { + // For completeness + profile.AllowUnknownConnections() + } + + // Start up the Profile + if acnStatus == 100 { + autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerAutostart) + if !exists || autostart == "true" { + eh.app.ActivatePeerEngine(onion) + } + } + + online, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline) + e.Data["Online"] = online + + autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerAutostart) + // legacy profiles should autostart by default + if !exists { + autostart = "true" + } + e.Data["autostart"] = autostart + + // Name always exists + e.Data[constants.Name], _ = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + e.Data[constants2.DefaultProfilePicture] = RandomProfileImage(onion) + // if a custom profile image exists then default to it. + key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) + if !exists { + e.Data[constants2.Picture] = RandomProfileImage(onion) + } else { + e.Data[constants2.Picture], _ = profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key)) + } + // Construct our conversations and our srever lists + var contacts []Contact + var servers []groups.Server + + conversations, err := profile.FetchConversations() + + if err == nil { + // We have conversations attached to this profile... + for _, conversationInfo := range conversations { + // Only compile the server info if we have enabled the experiment... + // Note that this means that this info can become stale if when first loaded the experiment + // has been disabled and then is later re-enabled. As such we need to ensure that this list is + // re-fetched when the group experiment is enabled via a dedicated ListServerInfo event... + if conversationInfo.IsServer() { + groupHandler, err := groups.ExperimentGate(eh.app.ReadSettings().Experiments) + if err == nil { + servers = append(servers, groupHandler.GetServerInfo(conversationInfo.Handle, profile)) + } + continue + } + + // Prefer local override to public name... + name, exists := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name) + if !exists { + name, exists = conversationInfo.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + if !exists { + name = conversationInfo.Handle + } + } + + // Resolve the profile image of the contact + var cpicPath string + var defaultPath string + if conversationInfo.IsGroup() { + cpicPath = RandomGroupImage(conversationInfo.Handle) + defaultPath = RandomGroupImage(conversationInfo.Handle) + } else { + cpicPath = GetProfileImage(profile, conversationInfo, settings.DownloadPath) + defaultPath = RandomProfileImage(conversationInfo.Handle) + } + + // Resolve Save History Setting + saveHistory, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, event.SaveHistoryKey) + if !set { + saveHistory = event.DeleteHistoryDefault + } + + // Resolve Archived Setting + isArchived, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants2.Archived) + if !set { + isArchived = event.False + } + + unread := 0 + lastSeenMessageId := -1 + lastSeenTimeStr, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants2.LastSeenTime) + if set { + lastSeenTime, err := time.Parse(constants2.DartIso8601, lastSeenTimeStr) + if err == nil { + // get last 100 messages and count how many are after the lastSeenTime (100 cus hte ui just shows 99+ after) + messages, err := profile.GetMostRecentMessages(conversationInfo.ID, 0, 0, 100) + if err == nil { + for _, message := range messages { + msgTime, err := time.Parse(time.RFC3339Nano, message.Attr[constants.AttrSentTimestamp]) + if err == nil { + if msgTime.UTC().After(lastSeenTime.UTC()) { + unread++ + } else { + lastSeenMessageId = message.ID + break + } + } + } + } + } + } + + groupServer, _ := conversationInfo.GetAttribute(attr.LocalScope, attr.LegacyGroupZone, constants.GroupServer) + + stateHandle := conversationInfo.Handle + if conversationInfo.IsGroup() { + stateHandle = groupServer + } + state := profile.GetPeerState(stateHandle) + if !set { + state = connections.DISCONNECTED + } + + blocked := false + if conversationInfo.ACL[conversationInfo.Handle].Blocked { + blocked = true + } + + // Fetch the message count, and the time of the most recent message + count, err := profile.GetChannelMessageCount(conversationInfo.ID, 0) + if err != nil { + log.Errorf("error fetching channel message count %v %v", conversationInfo.ID, err) + } + + lastMessage, _ := profile.GetMostRecentMessages(conversationInfo.ID, 0, 0, 1) + + notificationPolicy := constants2.ConversationNotificationPolicyDefault + if notificationPolicyAttr, exists := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants2.ConversationNotificationPolicy); exists { + notificationPolicy = notificationPolicyAttr + } + + contacts = append(contacts, Contact{ + Name: name, + Identifier: conversationInfo.ID, + Onion: conversationInfo.Handle, + Status: connections.ConnectionStateName[state], + Picture: cpicPath, + DefaultPicture: defaultPath, + Accepted: conversationInfo.Accepted, + AccessControlList: conversationInfo.ACL, + Blocked: blocked, + SaveHistory: saveHistory, + Messages: count, + Unread: unread, + LastSeenMessageId: lastSeenMessageId, + LastMessage: strconv.Itoa(getLastMessageTime(lastMessage)), + IsGroup: conversationInfo.IsGroup(), + GroupServer: groupServer, + IsArchived: isArchived == event.True, + NotificationPolicy: notificationPolicy, + Attributes: conversationInfo.Attributes, + }) + } + } + + 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) +} + +func GetProfileImage(profile peer.CwtchPeer, conversationInfo *model.Conversation, basepath string) string { + fileKey, err := profile.GetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.CustomProfileImageKey))) + if err == nil { + if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True { + fp, _ := filesharing.GenerateDownloadPath(basepath, fileKey, true) + // check if the file exists...if it does then set the path... + if _, err := os.Stat(fp); err == nil { + image, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)) + return image + } + } + } + return RandomProfileImage(conversationInfo.Handle) +} + +// handleProfileEvent enriches Profile events so they are usable with out further data fetches +func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string { + // cache of contact states to use to filter out events repeating known states + var contactStateCache = make(map[string]connections.ConnectionState) + + if eh.app == nil { + log.Errorf("eh.app == nil in handleProfileEvent... this shouldnt happen?") + } else { + profile := eh.app.GetPeer(ev.Profile) + log.Debugf("New Profile Event to Handle: %v", ev) + switch ev.Event.EventType { + case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data + // only needs contact nickname and picture, for displaying on popup notifications + ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"]) + ev.Event.Data[constants2.Picture] = RandomProfileImage(ev.Event.Data["RemotePeer"]) + if ci != nil && err == nil { + ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID) + profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False) + ev.Event.Data[constants2.Picture] = GetProfileImage(profile, ci, eh.app.ReadSettings().DownloadPath) + } else { + // TODO This Conversation May Not Exist Yet...But we are not in charge of creating it... + log.Errorf("todo wait for contact to be added before processing this event...") + return "" + } + var exists bool + ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name) + if !exists { + ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + if !exists { + ev.Event.Data["Nick"] = ev.Event.Data["RemotePeer"] + // If we dont have a name val for a peer, but they have sent us a message, we might be approved now, re-ask + profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.Name) + profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) + } + } + + if ci.Accepted { + handleImagePreviews(profile, &ev.Event, ci.ID, ci.ID, eh.app.ReadSettings()) + } + + ev.Event.Data["notification"] = string(determineNotification(ci, eh.app.ReadSettings())) + case event.NewMessageFromGroup: + // only needs contact nickname and picture, for displaying on popup notifications + ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"]) + ev.Event.Data[constants2.Picture] = RandomProfileImage(ev.Event.Data["RemotePeer"]) + if ci != nil && err == nil { + var exists bool + ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name) + if !exists { + ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + if !exists { + ev.Event.Data["Nick"] = ev.Event.Data["RemotePeer"] + } + } + ev.Event.Data[constants2.Picture] = GetProfileImage(profile, ci, eh.app.ReadSettings().DownloadPath) + } + + conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID]) + profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False) + + if ci != nil && ci.Accepted { + handleImagePreviews(profile, &ev.Event, conversationID, ci.ID, eh.app.ReadSettings()) + } + + gci, _ := profile.GetConversationInfo(conversationID) + groupServer := gci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupServer)).ToString()] + state := profile.GetPeerState(groupServer) + // if syncing, don't flood with notifications + if state == connections.SYNCED { + ev.Event.Data["notification"] = string(determineNotification(gci, eh.app.ReadSettings())) + } else { + ev.Event.Data["notification"] = string(constants2.NotificationNone) + } + case event.PeerAcknowledgement: + ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"]) + if ci != nil && err == nil { + ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID) + } + case event.ContactCreated: + conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID]) + count, err := profile.GetChannelMessageCount(conversationID, 0) + if err != nil { + log.Errorf("error fetching channel message count %v %v", conversationID, err) + } + + conversationInfo, err := profile.GetConversationInfo(conversationID) + if err != nil { + log.Errorf("error fetching conversation info for %v %v", conversationID, err) + } + + blocked := constants.False + if conversationInfo.ACL[conversationInfo.Handle].Blocked { + blocked = constants.True + } + + accepted := constants.False + if conversationInfo.Accepted { + accepted = constants.True + } + + acl, _ := json.Marshal(conversationInfo.ACL) + + lastMessage, _ := profile.GetMostRecentMessages(conversationID, 0, 0, 1) + ev.Event.Data["unread"] = strconv.Itoa(count) // if this is a new contact with messages attached then by-definition these are unread... + ev.Event.Data[constants2.Picture] = RandomProfileImage(conversationInfo.Handle) + ev.Event.Data[constants2.DefaultProfilePicture] = RandomProfileImage(conversationInfo.Handle) + ev.Event.Data["numMessages"] = strconv.Itoa(count) + ev.Event.Data["nick"] = conversationInfo.Handle + ev.Event.Data["status"] = connections.ConnectionStateName[profile.GetPeerState(conversationInfo.Handle)] + ev.Event.Data["accepted"] = accepted + ev.Event.Data["accessControlList"] = string(acl) + ev.Event.Data["blocked"] = blocked + ev.Event.Data["loading"] = "false" + ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(lastMessage)) + case event.GroupCreated: + // This event should only happen after we have validated the invite, as such the error + // condition *should* never happen. + groupPic := RandomGroupImage(ev.Event.Data[event.GroupID]) + ev.Event.Data[constants2.Picture] = groupPic + + conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID]) + conversationInfo, _ := profile.GetConversationInfo(conversationID) + acl, _ := json.Marshal(conversationInfo.ACL) + ev.Event.Data["accessControlList"] = string(acl) + 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 := RandomGroupImage(invite.GroupID) + ev.Event.Data[constants2.Picture] = groupPic + + conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID]) + conversationInfo, _ := profile.GetConversationInfo(conversationID) + acl, _ := json.Marshal(conversationInfo.ACL) + ev.Event.Data["accessControlList"] = string(acl) + } 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.openprivacy.ca/cwtch.im/cwtch", err) + return "" + } + case event.PeerStateChange: + cxnState := connections.ConnectionStateToType()[ev.Event.Data[event.ConnectionState]] + + // skip events the UI doesn't act on + if cxnState == connections.CONNECTING || cxnState == connections.CONNECTED { + return "" + } + + contact, err := profile.FetchConversationInfo(ev.Event.Data[event.RemotePeer]) + + if ev.Event.Data[event.RemotePeer] == profile.GetOnion() { + return "" // suppress events from our own profile... + } + + // We do not know who this is...don't send any event until we see a message from them + // (at that point the conversation will have been created...) + if contact == nil || err != nil || contact.ID == 0 { + return "" + } + + // if we already know this state, suppress + if knownState, exists := contactStateCache[ev.Event.Data[event.RemotePeer]]; exists && cxnState == knownState { + return "" + } + contactStateCache[ev.Event.Data[event.RemotePeer]] = cxnState + + if contact != nil { + // No enrichment needed + if cxnState == connections.AUTHENTICATED { + // if known and authed, get vars + profile.SendScopedZonedGetValToContact(contact.ID, attr.PublicScope, attr.ProfileZone, constants.Name) + profile.SendScopedZonedGetValToContact(contact.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) + } + } + case event.ServerStateChange: + cxnState := connections.ConnectionStateToType()[ev.Event.Data[event.ConnectionState]] + + // skip events the UI doesn't act on + if cxnState == connections.CONNECTING || cxnState == connections.CONNECTED { + return "" + } + + // if we already know this state, suppress + if knownState, exists := contactStateCache[ev.Event.Data[event.RemotePeer]]; exists && cxnState == knownState { + return "" + } + contactStateCache[ev.Event.Data[event.RemotePeer]] = cxnState + + 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]) + + conversation, err := profile.FetchConversationInfo(onion) + if err == nil { + + if exists && attr.IntoScope(scope) == attr.PublicScope { + zone, path := attr.ParseZone(path) + + // auto download profile images from contacts... + settings := eh.app.ReadSettings() + if settings.ExperimentsEnabled && zone == attr.ProfileZone && path == constants.CustomProfileImageKey { + fileKey := val + fsf, err := filesharing.FunctionalityGate(settings.Experiments) + imagePreviewsEnabled := settings.Experiments["filesharing-images"] + if err == nil && imagePreviewsEnabled && conversation.Accepted { + + basepath := settings.DownloadPath + fp, mp := filesharing.GenerateDownloadPath(basepath, fileKey, true) + + if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True { + if _, err := os.Stat(fp); err == nil { + // file is marked as completed downloaded and exists... + return "" + } else { + // the user probably deleted the file, mark completed as false... + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), event.False) + } + } + + log.Debugf("Downloading Profile Image %v %v %v", fp, mp, fileKey) + ev.Event.Data[event.FilePath] = fp + fsf.DownloadFile(profile, conversation.ID, fp, mp, val, constants.ImagePreviewMaxSizeInBytes) + } else { + // if image previews are disabled then ignore this event... + return "" + } + } + if val, err := profile.GetConversationAttribute(conversation.ID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(path))); err == nil || val != "" { + // we have a locally set override, don't pass this remote set public scope update to UI + return "" + } + } + } + case event.TokenManagerInfo: + conversations, err := profile.FetchConversations() + if err == nil { + var associatedGroups []int + for _, ci := range conversations { + groupServer, groupServerExists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupServer)).ToString()] + if groupServerExists { + gci, err := profile.FetchConversationInfo(groupServer) + if err == nil { + tokenOnion, onionExists := gci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypeTokenOnion))).ToString()] + if onionExists && tokenOnion == ev.Event.Data[event.ServerTokenOnion] { + associatedGroups = append(associatedGroups, ci.ID) + } + } + } + } + associatedGroupsJson, _ := json.Marshal(associatedGroups) + ev.Event.Data[event.Data] = string(associatedGroupsJson) + } + case event.ProtocolEngineCreated: + // TODO this code should be moved into Cwtch during the API officialization... + settings := eh.app.ReadSettings() + + // ensure that protocol engine respects blocking settings... + if settings.BlockUnknownConnections { + profile.BlockUnknownConnections() + } else { + profile.AllowUnknownConnections() + } + + // Now that the Peer Engine is Activated, Share Files + key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) + if exists { + serializedManifest, _ := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key)) + // reset the share timestamp, currently file shares are hardcoded to expire after 30 days... + // we reset the profile image here so that it is always available. + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10)) + log.Debugf("Custom Profile Image: %v %s", key, serializedManifest) + } + // If file sharing is enabled then reshare all active files... + fsf, err := filesharing.FunctionalityGate(settings.Experiments) + if err == nil { + fsf.ReShareFiles(profile) + } + } + } + + 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.NetworkStatus, q) + eventBus.Subscribe(event.ACNInfo, q) + eventBus.Subscribe(event.NewMessageFromPeer, q) + eventBus.Subscribe(event.UpdatedProfileAttribute, 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.ContactCreated, q) + eventBus.Subscribe(event.NewMessageFromGroup, q) + eventBus.Subscribe(event.GroupCreated, q) + eventBus.Subscribe(event.NewGroup, q) + eventBus.Subscribe(event.ServerStateChange, q) + eventBus.Subscribe(event.PeerStateChange, q) + eventBus.Subscribe(event.NewRetValMessageFromPeer, q) + eventBus.Subscribe(event.ShareManifest, q) + eventBus.Subscribe(event.ManifestSizeReceived, q) + eventBus.Subscribe(event.ManifestError, q) + eventBus.Subscribe(event.ManifestReceived, q) + eventBus.Subscribe(event.ManifestSaved, q) + eventBus.Subscribe(event.FileDownloadProgressUpdate, q) + eventBus.Subscribe(event.FileDownloaded, q) + eventBus.Subscribe(event.TokenManagerInfo, q) + eventBus.Subscribe(event.ProtocolEngineCreated, q) + go eh.forwardProfileMessages(onion, q) + +} + +func (eh *EventHandler) forwardProfileMessages(onion string, q event.Queue) { + log.Infof("Launching Forwarding Goroutine") + // 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 + } + } +} + +// Push pushes an event onto the app event bus +// +// It is also a way for libCwtch-go to publish an event for consumption by a UI before a Cwtch app has been initialized +// use: to signal an error before a cwtch app could be created +func (eh *EventHandler) Push(newEvent event.Event) { + eh.appBusQueue.Publish(newEvent) +} + +func getLastMessageTime(conversationMessages []model.ConversationMessage) int { + if len(conversationMessages) == 0 { + return 0 + } + time, err := time.Parse(time.RFC3339Nano, conversationMessages[0].Attr[constants.AttrSentTimestamp]) + if err != nil { + return 0 + } + return int(time.Unix()) +} + +// handleImagePreviews checks settings and, if appropriate, auto-downloads any images +func handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int, settings app.GlobalSettings) { + fh, err := filesharing.PreviewFunctionalityGate(settings.Experiments) + + // Short-circuit if file sharing is disabled + if err != nil { + return + } + + // Short-circuit failures + // Don't autodownload images if the download path does not exist. + if settings.DownloadPath == "" { + return + } + + // Don't autodownload images if the download path does not exist. + if _, err := os.Stat(settings.DownloadPath); os.IsNotExist(err) { + return + } + + // Now look at the image preview experiment + imagePreviewsEnabled := settings.Experiments["filesharing-images"] + if imagePreviewsEnabled { + var cm model.MessageWrapper + err := json.Unmarshal([]byte(ev.Data[event.Data]), &cm) + if err == nil && cm.Overlay == model.OverlayFileSharing { + var fm filesharing.OverlayMessage + err = json.Unmarshal([]byte(cm.Data), &fm) + if err == nil { + if fm.ShouldAutoDL() { + basepath := settings.DownloadPath + fp, mp := filesharing.GenerateDownloadPath(basepath, fm.Name, false) + log.Debugf("autodownloading file!") + ev.Data["Auto"] = constants.True + mID, _ := strconv.Atoi(ev.Data["Index"]) + profile.UpdateMessageAttribute(conversationID, 0, mID, constants.AttrDownloaded, constants.True) + fh.DownloadFile(profile, senderID, fp, mp, fm.FileKey(), constants.ImagePreviewMaxSizeInBytes) + } + } + } + } +} 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/notifications.go b/utils/notifications.go new file mode 100644 index 0000000..7983892 --- /dev/null +++ b/utils/notifications.go @@ -0,0 +1,51 @@ +package utils + +import ( + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "git.openprivacy.ca/cwtch.im/libcwtch-go/constants" +) + +func determineNotification(ci *model.Conversation, settings app.GlobalSettings) constants.NotificationType { + switch settings.NotificationPolicy { + case app.NotificationPolicyMute: + return constants.NotificationNone + case app.NotificationPolicyOptIn: + if ci != nil { + if policy, exists := ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.ConversationNotificationPolicy); exists { + switch policy { + case constants.ConversationNotificationPolicyDefault: + return constants.NotificationNone + case constants.ConversationNotificationPolicyNever: + return constants.NotificationNone + case constants.ConversationNotificationPolicyOptIn: + return notificationContentToNotificationType(settings.NotificationContent) + } + } + } + return constants.NotificationNone + case app.NotificationPolicyDefaultAll: + if ci != nil { + if policy, exists := ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.ConversationNotificationPolicy); exists { + switch policy { + case constants.ConversationNotificationPolicyNever: + return constants.NotificationNone + case constants.ConversationNotificationPolicyDefault: + fallthrough + case constants.ConversationNotificationPolicyOptIn: + return notificationContentToNotificationType(settings.NotificationContent) + } + } + } + return notificationContentToNotificationType(settings.NotificationContent) + } + return constants.NotificationNone +} + +func notificationContentToNotificationType(notificationContent string) constants.NotificationType { + if notificationContent == "NotificationContent.ContactInfo" { + return constants.NotificationConversation + } + return constants.NotificationEvent +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..7c8a161 --- /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 finding image for profile: %v %v %v\n", onion, err, barr) + return "assets/extra/openprivacy.png" + } + return "assets/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 finding image for group: %v %v %v\n", handle, err, barr) + return "assets/extra/openprivacy.png" + } + return "assets/servers/" + choices[int(barr[0])%len(choices)] + ".png" +}