Initial Commit
This commit is contained in:
commit
0ecda3d3d5
|
@ -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/
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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"
|
|
@ -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"
|
|
@ -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}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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:<name>\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:<name>\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:<name>\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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,282 @@
|
||||||
|
//package cwtch
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// //Needed to invoke C.free
|
||||||
|
// #include <stdlib.h>
|
||||||
|
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() {}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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")
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
Loading…
Reference in New Issue