Updated Cwtch Refactor
This commit is contained in:
parent
86c5a51b22
commit
dcdbf382cb
|
@ -1,44 +0,0 @@
|
|||
package contact
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/features"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
)
|
||||
|
||||
// Functionality groups some common UI triggered functions for contacts...
|
||||
type Functionality struct {
|
||||
}
|
||||
|
||||
const addContactPrefix = "addcontact"
|
||||
|
||||
const sendMessagePrefix = "sendmessage"
|
||||
|
||||
// FunctionalityGate returns contact.Functionality always
|
||||
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
||||
return new(Functionality), nil
|
||||
}
|
||||
|
||||
// SendMessage handles sending messages to contacts
|
||||
func (pf *Functionality) SendMessage(peer peer.SendMessages, handle string, message string) features.Response {
|
||||
err := peer.SendMessage(handle, message)
|
||||
if err == nil {
|
||||
return features.ConstructResponse(sendMessagePrefix, "success")
|
||||
}
|
||||
return features.ConstructResponse(sendMessagePrefix, err.Error())
|
||||
}
|
||||
|
||||
// HandleImportString handles contact import strings
|
||||
func (pf *Functionality) HandleImportString(peer peer.ModifyContactsAndPeers, importString string) features.Response {
|
||||
if tor.IsValidHostname(importString) {
|
||||
if peer.GetContact(importString) == nil {
|
||||
peer.AddContact(importString, importString, model.AuthApproved)
|
||||
// Implicit Peer Attempt
|
||||
peer.PeerWithOnion(importString)
|
||||
return features.ConstructResponse(addContactPrefix, "success")
|
||||
}
|
||||
return features.ConstructResponse(addContactPrefix, "contact_already_exists")
|
||||
}
|
||||
return features.ConstructResponse(addContactPrefix, "invalid_import_string")
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
package contact
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/features"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const ValidHostname = "openpravyvc6spbd4flzn4g2iqu4sxzsizbtb5aqec25t76dnoo5w7yd"
|
||||
|
||||
type MockPeer struct {
|
||||
hasContact bool
|
||||
addContact bool
|
||||
peerRequest bool
|
||||
}
|
||||
|
||||
func (m MockPeer) BlockUnknownConnections() {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m MockPeer) AllowUnknownConnections() {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m MockPeer) GetContacts() []string {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m MockPeer) GetContact(s string) *model.PublicProfile {
|
||||
if m.hasContact {
|
||||
return &(model.GenerateNewProfile("").PublicProfile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m MockPeer) GetContactAttribute(s string, s2 string) (string, bool) {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m *MockPeer) AddContact(nick, onion string, authorization model.Authorization) {
|
||||
m.addContact = true
|
||||
}
|
||||
|
||||
func (m MockPeer) SetContactAuthorization(s string, authorization model.Authorization) error {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m MockPeer) SetContactAttribute(s string, s2 string, s3 string) {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m MockPeer) DeleteContact(s string) {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (m *MockPeer) PeerWithOnion(s string) {
|
||||
m.peerRequest = true
|
||||
}
|
||||
|
||||
func (m MockPeer) JoinServer(s string) error {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func TestContactFunctionality_InValidHostname(t *testing.T) {
|
||||
cf, _ := FunctionalityGate(map[string]bool{})
|
||||
|
||||
peer := &MockPeer{
|
||||
hasContact: false,
|
||||
addContact: false,
|
||||
peerRequest: false,
|
||||
}
|
||||
|
||||
response := cf.HandleImportString(peer, "")
|
||||
|
||||
if peer.addContact || peer.peerRequest {
|
||||
t.Fatalf("HandleImportString for a malformed import string should have no resulted in addContact or a peerRequest: %v", peer)
|
||||
}
|
||||
|
||||
if response.Error() != features.ConstructResponse(addContactPrefix, "invalid_import_string").Error() {
|
||||
t.Fatalf("Response to a successful import is malformed: %v", response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestContactFunctionality_ValidHostnameExistingContact(t *testing.T) {
|
||||
cf, _ := FunctionalityGate(map[string]bool{})
|
||||
|
||||
peer := &MockPeer{
|
||||
hasContact: true,
|
||||
addContact: false,
|
||||
peerRequest: false,
|
||||
}
|
||||
|
||||
response := cf.HandleImportString(peer, ValidHostname)
|
||||
|
||||
if peer.addContact || peer.peerRequest {
|
||||
t.Fatalf("HandleImportString for a valid string should not call addContact or a peerRequest when the contact already exists: %v", peer)
|
||||
}
|
||||
|
||||
if response.Error() != features.ConstructResponse(addContactPrefix, "contact_already_exists").Error() {
|
||||
t.Fatalf("Response to a successful import is malformed: %v", response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestContactFunctionality_ValidHostnameUnknownContact(t *testing.T) {
|
||||
cf, _ := FunctionalityGate(map[string]bool{})
|
||||
|
||||
peer := &MockPeer{
|
||||
hasContact: false,
|
||||
addContact: false,
|
||||
peerRequest: false,
|
||||
}
|
||||
|
||||
response := cf.HandleImportString(peer, ValidHostname)
|
||||
|
||||
if peer.addContact && peer.peerRequest {
|
||||
if response.Error() != features.ConstructResponse(addContactPrefix, "success").Error() {
|
||||
t.Fatalf("Response to a successful import is malformed: %v", response)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("HandleImportString for a valid import string should have resulted in addContact or a peerRequest: %v", peer)
|
||||
}
|
||||
}
|
|
@ -3,12 +3,10 @@ package groups
|
|||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"encoding/base64"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/features"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const serverPrefix = "server:"
|
||||
|
@ -28,12 +26,6 @@ const (
|
|||
UpdateServerInfo = event.Type("UpdateServerInfo")
|
||||
)
|
||||
|
||||
// ReadServerInfo is a meta-interface for reading information about servers..
|
||||
type ReadServerInfo interface {
|
||||
peer.ReadContacts
|
||||
peer.ReadServers
|
||||
}
|
||||
|
||||
// GroupFunctionality provides experiment gated server functionality
|
||||
type GroupFunctionality struct {
|
||||
}
|
||||
|
@ -46,27 +38,8 @@ func ExperimentGate(experimentMap map[string]bool) (*GroupFunctionality, error)
|
|||
return nil, fmt.Errorf("gated by %v", groupExperiment)
|
||||
}
|
||||
|
||||
// SendMessage is a deprecated api
|
||||
func (gf *GroupFunctionality) SendMessage(peer peer.CwtchPeer, handle string, message string) (string, error) {
|
||||
// TODO this auto accepting behaviour needs some thinking through
|
||||
if !peer.GetGroup(handle).Accepted {
|
||||
err := peer.AcceptInvite(handle)
|
||||
if err != nil {
|
||||
log.Errorf("tried to mark a nonexistent group as existed. bad!")
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", peer.SendMessage(handle, message)
|
||||
}
|
||||
|
||||
// ValidPrefix returns true if an import string contains a prefix that indicates it contains information about a
|
||||
// server or a group
|
||||
func (gf *GroupFunctionality) ValidPrefix(importString string) bool {
|
||||
return strings.HasPrefix(importString, tofuBundlePrefix) || strings.HasPrefix(importString, serverPrefix) || strings.HasPrefix(importString, groupPrefix)
|
||||
}
|
||||
|
||||
// GetServerInfoList compiles all the information the UI might need regarding all servers..
|
||||
func (gf *GroupFunctionality) GetServerInfoList(profile ReadServerInfo) []Server {
|
||||
func (gf *GroupFunctionality) GetServerInfoList(profile peer.CwtchPeer) []Server {
|
||||
var servers []Server
|
||||
for _, server := range profile.GetServers() {
|
||||
servers = append(servers, gf.GetServerInfo(server, profile))
|
||||
|
@ -76,52 +49,15 @@ func (gf *GroupFunctionality) GetServerInfoList(profile ReadServerInfo) []Server
|
|||
|
||||
// GetServerInfo compiles all the information the UI might need regarding a particular server including any verified
|
||||
// cryptographic keys
|
||||
func (gf *GroupFunctionality) GetServerInfo(serverOnion string, profile peer.ReadContacts) Server {
|
||||
serverInfo := profile.GetContact(serverOnion)
|
||||
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(string(keyType)); has {
|
||||
if key, has := serverInfo.GetAttribute(attr.PublicScope, attr.ServerKeyZone, string(keyType)); has {
|
||||
serverKeys = append(serverKeys, ServerKey{Type: string(keyType), Key: key})
|
||||
}
|
||||
}
|
||||
return Server{Onion: serverOnion, Status: serverInfo.State, Keys: serverKeys}
|
||||
}
|
||||
|
||||
// HandleImportString handles import strings for groups and servers
|
||||
func (gf *GroupFunctionality) HandleImportString(peer peer.CwtchPeer, importString string) error {
|
||||
if strings.HasPrefix(importString, tofuBundlePrefix) {
|
||||
bundle := strings.Split(importString, "||")
|
||||
if len(bundle) == 2 {
|
||||
err := gf.HandleImportString(peer, bundle[0][len(tofuBundlePrefix):])
|
||||
// if the server import failed then abort the whole process..
|
||||
if err != nil && !strings.HasSuffix(err.Error(), "success") {
|
||||
return features.ConstructResponse(importBundlePrefix, err.Error())
|
||||
}
|
||||
return gf.HandleImportString(peer, bundle[1])
|
||||
}
|
||||
} else if strings.HasPrefix(importString, serverPrefix) {
|
||||
// Server Key Bundles are prefixed with
|
||||
bundle, err := base64.StdEncoding.DecodeString(importString[len(serverPrefix):])
|
||||
if err == nil {
|
||||
if err = peer.AddServer(string(bundle)); err != nil {
|
||||
return features.ConstructResponse(importBundlePrefix, err.Error())
|
||||
}
|
||||
return features.ConstructResponse(importBundlePrefix, "success")
|
||||
}
|
||||
return features.ConstructResponse(importBundlePrefix, err.Error())
|
||||
} else if strings.HasPrefix(importString, groupPrefix) {
|
||||
//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
|
||||
if gid, err := peer.ImportGroup(importString); err != nil {
|
||||
return features.ConstructResponse(importBundlePrefix, err.Error())
|
||||
} else {
|
||||
// Auto accept the group here.
|
||||
if peer.AcceptInvite(gid) != nil {
|
||||
log.Errorf("Error accepting invite: %v", err)
|
||||
}
|
||||
return features.ConstructResponse(importBundlePrefix, "success")
|
||||
}
|
||||
}
|
||||
return features.ConstructResponse(importBundlePrefix, "invalid_group_invite_prefix")
|
||||
}
|
||||
return Server{Onion: serverOnion, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys}
|
||||
}
|
|
@ -15,8 +15,8 @@ import (
|
|||
const serversExperiment = "servers-experiment"
|
||||
|
||||
const (
|
||||
ZeroServersLoaded = event.Type("ZeroServersLoaded")
|
||||
NewServer = event.Type("NewServer")
|
||||
ZeroServersLoaded = event.Type("ZeroServersLoaded")
|
||||
NewServer = event.Type("NewServer")
|
||||
ServerIntentUpdate = event.Type("ServerIntentUpdate")
|
||||
ServerDeleted = event.Type("ServerDeleted")
|
||||
)
|
||||
|
@ -31,12 +31,12 @@ const (
|
|||
)
|
||||
|
||||
type ServerInfo struct {
|
||||
Onion string
|
||||
Onion string
|
||||
ServerBundle string
|
||||
Autostart bool
|
||||
Running bool
|
||||
Description string
|
||||
StorageType string
|
||||
Autostart bool
|
||||
Running bool
|
||||
Description string
|
||||
StorageType string
|
||||
}
|
||||
|
||||
var lock sync.Mutex
|
||||
|
@ -72,7 +72,7 @@ type ServersFunctionality struct {
|
|||
func ExperimentGate(experimentMap map[string]bool) (*ServersFunctionality, error) {
|
||||
if experimentMap[serversExperiment] {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
defer lock.Unlock()
|
||||
return &ServersFunctionality{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("gated by %v", serversExperiment)
|
||||
|
|
2
go.mod
2
go.mod
|
@ -11,3 +11,5 @@ require (
|
|||
golang.org/x/mod v0.5.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
|
||||
)
|
||||
|
||||
replace cwtch.im/cwtch => /home/sarah/workspace/src/cwtch.im/cwtch
|
3
go.sum
3
go.sum
|
@ -1,7 +1,4 @@
|
|||
cwtch.im/cwtch v0.12.2 h1:I+ndKadCRCITw4SPbd+1cpRv+z/7iHjjTUv8OzRwTrE=
|
||||
cwtch.im/cwtch v0.12.2/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM=
|
||||
cwtch.im/cwtch v0.13.0 h1:9WhLG9czfyRceZnHSqfTAq0vfmDC/E20mziJb9+Skrg=
|
||||
cwtch.im/cwtch v0.13.0/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM=
|
||||
cwtch.im/cwtch v0.13.1 h1:k7CDr16ZLZ+uaRtic2Joooc8TzkO7BkgWXs8r9ilqDY=
|
||||
cwtch.im/cwtch v0.13.1/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM=
|
||||
cwtch.im/cwtch v0.13.2 h1:qbKTQOUryvJpTzIf5iT49x6iAmeNxiz0doNb5phYVEQ=
|
||||
|
|
259
lib.go
259
lib.go
|
@ -1,6 +1,6 @@
|
|||
//package cwtch
|
||||
package cwtch
|
||||
|
||||
package main
|
||||
//package main
|
||||
|
||||
// //Needed to invoke C.free
|
||||
// #include <stdlib.h>
|
||||
|
@ -8,6 +8,7 @@ import "C"
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
constants2 "cwtch.im/cwtch/model/constants"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/user"
|
||||
|
@ -23,7 +24,6 @@ import (
|
|||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/constants"
|
||||
contact "git.openprivacy.ca/cwtch.im/libcwtch-go/features/contacts"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/features/groups"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/features/servers"
|
||||
"git.openprivacy.ca/cwtch.im/server"
|
||||
|
@ -246,34 +246,25 @@ func ReconnectCwtchForeground() {
|
|||
}
|
||||
|
||||
settings := utils.ReadGlobalSettings()
|
||||
|
||||
groupHandler,_ := groups.ExperimentGate(settings.Experiments)
|
||||
for _, profileOnion := range peerList {
|
||||
// fix peerpeercontact message counts
|
||||
contactList := application.GetPeer(profileOnion).GetContacts()
|
||||
for _, handle := range contactList {
|
||||
totalMessages := application.GetPeer(profileOnion).GetContact(handle).Timeline.Len() + len(application.GetPeer(profileOnion).GetContact(handle).UnacknowledgedMessages)
|
||||
eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{
|
||||
event.Identity: profileOnion,
|
||||
event.RemotePeer: handle,
|
||||
event.Data: strconv.Itoa(totalMessages),
|
||||
}))
|
||||
}
|
||||
|
||||
// Group Experiment Refresh
|
||||
groupHandler, err := groups.ExperimentGate(settings.Experiments)
|
||||
if err == nil {
|
||||
// fix peergroupcontact message counts
|
||||
groupList := application.GetPeer(profileOnion).GetGroups()
|
||||
for _, groupID := range groupList {
|
||||
totalMessages := len(application.GetPeer(profileOnion).GetGroup(groupID).GetTimeline())
|
||||
profile := application.GetPeer(profileOnion)
|
||||
conversations,_ := profile.FetchConversations()
|
||||
for _, conversation := range conversations {
|
||||
if (conversation.IsGroup() && groupHandler != nil) || conversation.IsServer() == false {
|
||||
totalMessages,_ := profile.GetChannelMessageCount(conversation.ID, 0)
|
||||
eventHandler.Push(event.NewEvent(event.MessageCounterResync, map[event.Field]string{
|
||||
event.Identity: profileOnion,
|
||||
event.GroupID: groupID,
|
||||
event.Data: strconv.Itoa(totalMessages),
|
||||
event.Identity: profileOnion,
|
||||
event.ConversationID: strconv.Itoa(conversation.ID),
|
||||
event.Data: strconv.Itoa(totalMessages),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
serverListForOnion := groupHandler.GetServerInfoList(application.GetPeer(profileOnion))
|
||||
// Group Experiment Server Refresh
|
||||
if groupHandler != nil {
|
||||
serverListForOnion := groupHandler.GetServerInfoList(profile)
|
||||
serversListBytes, _ := json.Marshal(serverListForOnion)
|
||||
eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)}))
|
||||
}
|
||||
|
@ -412,25 +403,6 @@ func SendProfileEvent(onion string, eventJson string) {
|
|||
// We need to update the local cache
|
||||
// Ideally I think this would be pushed back into Cwtch
|
||||
switch new_event.EventType {
|
||||
// DEPRECATED: use ImportBundle
|
||||
case AddContact:
|
||||
pf, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments)
|
||||
err := pf.HandleImportString(peer, new_event.Data[ImportString])
|
||||
eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: err.Error()}))
|
||||
// DEPRECATED: use SetProfileAttribute()
|
||||
case event.SetAttribute:
|
||||
log.Errorf("SetAttribute is deprecated.")
|
||||
// DEPRECATED: use SetContactAttribute()
|
||||
case event.SetPeerAttribute:
|
||||
peer.SetContactAttribute(new_event.Data[event.RemotePeer], new_event.Data[event.Key], new_event.Data[event.Data])
|
||||
// DEPRECATED: use AcceptContact() and BlockContact()
|
||||
case event.SetPeerAuthorization:
|
||||
peer.SetContactAuthorization(new_event.Data[event.RemotePeer], model.Authorization(new_event.Data[event.Authorization]))
|
||||
|
||||
// If approved (e.g. after an unblock) we want to kick off peering again...
|
||||
if model.Authorization(new_event.Data[event.Authorization]) == model.AuthApproved {
|
||||
peer.PeerWithOnion(new_event.Data[event.RemotePeer])
|
||||
}
|
||||
default:
|
||||
// rebroadcast catch all
|
||||
log.Infof("Received Event %v for %v but no libCwtch handler found, relaying the event directly", new_event, onion)
|
||||
|
@ -480,64 +452,26 @@ func LoadProfiles(pass string) {
|
|||
application.LoadProfiles(pass)
|
||||
}
|
||||
|
||||
//export c_AcceptContact
|
||||
func c_AcceptContact(profilePtr *C.char, profileLen C.int, handlePtr *C.char, handleLen C.int) {
|
||||
AcceptContact(C.GoStringN(profilePtr, profileLen), C.GoStringN(handlePtr, handleLen))
|
||||
//export c_c_AcceptConversations
|
||||
func c_AcceptConversations(profilePtr *C.char, profileLen C.int, conversationID C.int) {
|
||||
AcceptConversations(C.GoStringN(profilePtr, profileLen), conversationID)
|
||||
}
|
||||
|
||||
// AcceptContact takes in a profileOnion and a handle to either a group or a peer and authorizes the handle
|
||||
// AcceptConversations takes in a profileOnion and a handle to either a group or a peer and authorizes the handle
|
||||
// for further action (e.g. messaging / connecting to the server / joining the group etc.)
|
||||
func AcceptContact(profileOnion string, handle string) {
|
||||
func AcceptConversations(profileOnion string, conversationID int) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
profileHandler := utils.NewPeerHelper(profile)
|
||||
if profileHandler.IsGroup(handle) {
|
||||
profile.AcceptInvite(handle)
|
||||
} else {
|
||||
err := profile.SetContactAuthorization(handle, model.AuthApproved)
|
||||
if err == nil {
|
||||
eventHandler.Push(event.NewEvent(event.PeerStateChange, map[event.Field]string{
|
||||
ProfileOnion: profileOnion,
|
||||
event.RemotePeer: handle,
|
||||
"authorization": string(model.AuthApproved),
|
||||
}))
|
||||
} else {
|
||||
log.Errorf("error accepting contact: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//export c_RejectInvite
|
||||
func c_RejectInvite(profilePtr *C.char, profileLen C.int, handlePtr *C.char, handleLen C.int) {
|
||||
RejectInvite(C.GoStringN(profilePtr, profileLen), C.GoStringN(handlePtr, handleLen))
|
||||
}
|
||||
|
||||
// RejectInvite rejects a group invite
|
||||
func RejectInvite(profileOnion string, handle string) {
|
||||
log.Debugf("rejecting invite %v for %v", handle, profileOnion)
|
||||
profile := application.GetPeer(profileOnion)
|
||||
profileHandler := utils.NewPeerHelper(profile)
|
||||
if profileHandler.IsGroup(handle) {
|
||||
profile.RejectInvite(handle)
|
||||
log.Debugf("successfully rejected invite %v for %v", handle, profileOnion)
|
||||
}
|
||||
profile.AcceptConversation(conversationID)
|
||||
}
|
||||
|
||||
//export c_BlockContact
|
||||
func c_BlockContact(profilePtr *C.char, profileLen C.int, handlePtr *C.char, handleLen C.int) {
|
||||
BlockContact(C.GoStringN(profilePtr, profileLen), C.GoStringN(handlePtr, handleLen))
|
||||
func c_BlockContact(profilePtr *C.char, profileLen C.int, conversationID C.int) {
|
||||
BlockContact(C.GoStringN(profilePtr, profileLen), conversationID)
|
||||
}
|
||||
|
||||
func BlockContact(profile, handle string) {
|
||||
err := application.GetPeer(profile).SetContactAuthorization(handle, model.AuthBlocked)
|
||||
if err == nil {
|
||||
eventHandler.Push(event.NewEvent(event.PeerStateChange, map[event.Field]string{
|
||||
ProfileOnion: profile,
|
||||
event.RemotePeer: handle,
|
||||
"authorization": string(model.AuthBlocked),
|
||||
}))
|
||||
} else {
|
||||
log.Errorf("error blocking contact: %s", err.Error())
|
||||
}
|
||||
func BlockContact(profileOnion string, conversationID int) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
profile.BlockConversation(conversationID)
|
||||
}
|
||||
|
||||
//export c_UpdateMessageFlags
|
||||
|
@ -655,78 +589,45 @@ func GetMessagesByContentHash(profileOnion, handle string, contentHash string) s
|
|||
}
|
||||
|
||||
//export c_SendMessage
|
||||
func c_SendMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, msg_ptr *C.char, msg_len C.int) {
|
||||
func c_SendMessage(profile_ptr *C.char, profile_len C.int, conversation_id C.int, msg_ptr *C.char, msg_len C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
handle := C.GoStringN(handle_ptr, handle_len)
|
||||
msg := C.GoStringN(msg_ptr, msg_len)
|
||||
SendMessage(profile, handle, msg)
|
||||
SendMessage(profile, conversation_id, msg)
|
||||
}
|
||||
|
||||
func SendMessage(profileOnion, handle, msg string) {
|
||||
func SendMessage(profileOnion string, conversationID int, msg string) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
ph := utils.NewPeerHelper(profile)
|
||||
if ph.IsGroup(handle) {
|
||||
groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err == nil {
|
||||
groupHandler.SendMessage(profile, handle, msg)
|
||||
profile.SetGroupAttribute(handle, attr.GetLocalScope(constants.Archived), event.False)
|
||||
}
|
||||
} else {
|
||||
contactHandler, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments)
|
||||
contactHandler.SendMessage(profile, handle, msg)
|
||||
profile.SetContactAttribute(handle, attr.GetLocalScope(constants.Archived), event.False)
|
||||
}
|
||||
profile.SendMessage(conversationID, msg)
|
||||
}
|
||||
|
||||
//export c_SendInvitation
|
||||
func c_SendInvitation(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, target_ptr *C.char, target_len C.int) {
|
||||
func c_SendInvitation(profile_ptr *C.char, profile_len C.int, conversation_id C.int, target_id C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
handle := C.GoStringN(handle_ptr, handle_len)
|
||||
target := C.GoStringN(target_ptr, target_len)
|
||||
SendInvitation(profile, handle, target)
|
||||
SendInvitation(profile, conversation_id, target_id)
|
||||
}
|
||||
|
||||
// Send an invitation from `profileOnion` to contact `handle` (peer or group)
|
||||
// SendInvitation sends an invitation from `profileOnion` to contact `handle` (peer or group)
|
||||
// asking them to add the contact `target` (also peer or group).
|
||||
// For groups, the profile must already have `target` as a contact.
|
||||
func SendInvitation(profileOnion, handle, target string) {
|
||||
func SendInvitation(profileOnion string, conversationID int, targetID int) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
ph := utils.NewPeerHelper(profile)
|
||||
|
||||
var invite ChatMessage
|
||||
if ph.IsGroup(target) {
|
||||
bundle, _ := profile.GetContact(profile.GetGroup(target).GroupServer).GetAttribute(string(model.BundleType))
|
||||
inviteStr, err := profile.GetGroup(target).Invite()
|
||||
if err == nil {
|
||||
invite = ChatMessage{O: 101, D: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), inviteStr)}
|
||||
}
|
||||
} else {
|
||||
invite = ChatMessage{O: 100, D: target}
|
||||
}
|
||||
|
||||
inviteBytes, err := json.Marshal(invite)
|
||||
if err != nil {
|
||||
log.Errorf("malformed invite: %v", err)
|
||||
} else {
|
||||
SendMessage(profileOnion, handle, string(inviteBytes))
|
||||
}
|
||||
profile.SendInviteToConversation(conversationID, targetID)
|
||||
}
|
||||
|
||||
//export c_ShareFile
|
||||
func c_ShareFile(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, filepath_ptr *C.char, filepath_len C.int) {
|
||||
func c_ShareFile(profile_ptr *C.char, profile_len C.int, conversationID C.int, filepath_ptr *C.char, filepath_len C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
handle := C.GoStringN(handle_ptr, handle_len)
|
||||
sharefilepath := C.GoStringN(filepath_ptr, filepath_len)
|
||||
ShareFile(profile, handle, sharefilepath)
|
||||
ShareFile(profile, conversationID, sharefilepath)
|
||||
}
|
||||
|
||||
func ShareFile(profileOnion, handle, sharefilepath string) {
|
||||
func ShareFile(profileOnion string , conversationID int, sharefilepath string) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
fh, err := filesharing.FunctionalityGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err != nil {
|
||||
log.Errorf("file sharing error: %v", err)
|
||||
} else {
|
||||
err = fh.ShareFile(sharefilepath, profile, handle)
|
||||
err = fh.ShareFile(sharefilepath, profile, conversationID)
|
||||
if err != nil {
|
||||
log.Errorf("error sharing file: %v", err)
|
||||
}
|
||||
|
@ -734,22 +635,21 @@ func ShareFile(profileOnion, handle, sharefilepath string) {
|
|||
}
|
||||
|
||||
//export c_DownloadFile
|
||||
func c_DownloadFile(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, filepath_ptr *C.char, filepath_len C.int, manifestpath_ptr *C.char, manifestpath_len C.int, filekey_ptr *C.char, filekey_len C.int) {
|
||||
func c_DownloadFile(profile_ptr *C.char, profile_len C.int, conversationID C.int, filepath_ptr *C.char, filepath_len C.int, manifestpath_ptr *C.char, manifestpath_len C.int, filekey_ptr *C.char, filekey_len C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
handle := C.GoStringN(handle_ptr, handle_len)
|
||||
downloadfilepath := C.GoStringN(filepath_ptr, filepath_len)
|
||||
manifestpath := C.GoStringN(manifestpath_ptr, manifestpath_len)
|
||||
filekey := C.GoStringN(filekey_ptr, filekey_len)
|
||||
DownloadFile(profile, handle, downloadfilepath, manifestpath, filekey)
|
||||
DownloadFile(profile, conversationID, downloadfilepath, manifestpath, filekey)
|
||||
}
|
||||
|
||||
func DownloadFile(profileOnion, handle, filepath, manifestpath, filekey string) {
|
||||
func DownloadFile(profileOnion string, conversationID int, filepath, manifestpath, filekey string) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
fh, err := filesharing.FunctionalityGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err != nil {
|
||||
log.Errorf("file sharing error: %v", err)
|
||||
} else {
|
||||
fh.DownloadFile(profile, handle, filepath, manifestpath, filekey)
|
||||
fh.DownloadFile(profile, conversationID, filepath, manifestpath, filekey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -781,19 +681,18 @@ func CheckDownloadStatus(profileOnion, fileKey string) {
|
|||
}
|
||||
|
||||
//export c_VerifyOrResumeDownload
|
||||
func c_VerifyOrResumeDownload(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, filekey_ptr *C.char, filekey_len C.int) {
|
||||
func c_VerifyOrResumeDownload(profile_ptr *C.char, profile_len C.int, conversationID, filekey_ptr *C.char, filekey_len C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
handle := C.GoStringN(handle_ptr, handle_len)
|
||||
filekey := C.GoStringN(filekey_ptr, filekey_len)
|
||||
VerifyOrResumeDownload(profile, handle, filekey)
|
||||
VerifyOrResumeDownload(profile, conversationID, filekey)
|
||||
}
|
||||
|
||||
func VerifyOrResumeDownload(profileOnion, handle, fileKey string) {
|
||||
func VerifyOrResumeDownload(profileOnion string, conversationID int, fileKey string) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)); exists {
|
||||
if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists {
|
||||
log.Infof("resuming %s", fileKey)
|
||||
DownloadFile(profileOnion, handle, downloadfilepath, manifestFilePath, fileKey)
|
||||
DownloadFile(profileOnion, conversationID, downloadfilepath, manifestFilePath, fileKey)
|
||||
} else {
|
||||
log.Errorf("found manifest path but not download path for %s", fileKey)
|
||||
}
|
||||
|
@ -824,11 +723,9 @@ func CreateGroup(profileHandle string, server string, name string) {
|
|||
profile := application.GetPeer(profileHandle)
|
||||
_, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err == nil {
|
||||
gid, _, err := profile.StartGroup(server)
|
||||
conversationID, err := profile.StartGroup(name, server)
|
||||
if err == nil {
|
||||
log.Debugf("created group %v on %v: $v", profileHandle, server, gid)
|
||||
// set the group name
|
||||
profile.SetGroupAttribute(gid, attr.GetLocalScope("name"), name)
|
||||
log.Debugf("created group %v on %v: $v", profileHandle, server, conversationID)
|
||||
} else {
|
||||
log.Errorf("error creating group or %v on server %v: %v", profileHandle, server, err)
|
||||
}
|
||||
|
@ -854,42 +751,27 @@ func DeleteProfile(profile string, password string) {
|
|||
}
|
||||
|
||||
//export c_ArchiveConversation
|
||||
func c_ArchiveConversation(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) {
|
||||
func c_ArchiveConversation(profile_ptr *C.char, profile_len C.int, conversationID C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
handle := C.GoStringN(handle_ptr, handle_len)
|
||||
ArchiveConversation(profile, handle)
|
||||
ArchiveConversation(profile, conversationID)
|
||||
}
|
||||
|
||||
// ArchiveConversation sets the conversation to archived
|
||||
func ArchiveConversation(profileHandle string, handle string) {
|
||||
func ArchiveConversation(profileHandle string, conversationID int) {
|
||||
profile := application.GetPeer(profileHandle)
|
||||
ph := utils.NewPeerHelper(profile)
|
||||
if ph.IsGroup(handle) {
|
||||
profile.SetGroupAttribute(handle, attr.GetLocalScope(constants.Archived), event.True)
|
||||
} else {
|
||||
profile.SetContactAttribute(handle, attr.GetLocalScope(constants.Archived), event.True)
|
||||
}
|
||||
profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Archived)), constants2.True)
|
||||
}
|
||||
|
||||
//export c_DeleteContact
|
||||
func c_DeleteContact(profile_ptr *C.char, profile_len C.int, hanlde_ptr *C.char, handle_len C.int) {
|
||||
func c_DeleteContact(profile_ptr *C.char, profile_len C.int, conversationID C.int) {
|
||||
profile := C.GoStringN(profile_ptr, profile_len)
|
||||
groupID := C.GoStringN(hanlde_ptr, handle_len)
|
||||
DeleteContact(profile, groupID)
|
||||
DeleteContact(profile, conversationID)
|
||||
}
|
||||
|
||||
// DeleteContact removes all trace of the contact from the profile
|
||||
func DeleteContact(profileHandle string, handle string) {
|
||||
func DeleteContact(profileHandle string, conversationID int) {
|
||||
profile := application.GetPeer(profileHandle)
|
||||
ph := utils.NewPeerHelper(profile)
|
||||
if ph.IsGroup(handle) {
|
||||
_, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err == nil {
|
||||
profile.DeleteGroup(handle)
|
||||
}
|
||||
} else {
|
||||
profile.DeleteContact(handle)
|
||||
}
|
||||
profile.DeleteConversation(conversationID)
|
||||
}
|
||||
|
||||
//export c_ImportBundle
|
||||
|
@ -903,21 +785,16 @@ func c_ImportBundle(profile_ptr *C.char, profile_len C.int, bundle_ptr *C.char,
|
|||
// different formats (e.g. a peer address, a group invite, a server key bundle, or a combination)
|
||||
func ImportBundle(profileOnion string, bundle string) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
peerHandler, _ := contact.FunctionalityGate(utils.ReadGlobalSettings().Experiments)
|
||||
response := peerHandler.HandleImportString(profile, bundle)
|
||||
if strings.Contains(response.Error(), "invalid_import_string") {
|
||||
groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err == nil {
|
||||
response = groupHandler.HandleImportString(profile, bundle)
|
||||
eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: response.Error()}))
|
||||
response := profile.ImportBundle(bundle)
|
||||
|
||||
// We might have added a new server, so refresh the server list...
|
||||
serverListForOnion := groupHandler.GetServerInfoList(profile)
|
||||
serversListBytes, _ := json.Marshal(serverListForOnion)
|
||||
eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)}))
|
||||
return
|
||||
}
|
||||
// We might have added a new server, so refresh the server list if applicable...
|
||||
groupHandler, err := groups.ExperimentGate(utils.ReadGlobalSettings().Experiments)
|
||||
if err == nil {
|
||||
serverListForOnion := groupHandler.GetServerInfoList(profile)
|
||||
serversListBytes, _ := json.Marshal(serverListForOnion)
|
||||
eventHandler.Push(event.NewEvent(groups.UpdateServerInfo, map[event.Field]string{"ProfileOnion": profileOnion, groups.ServerList: string(serversListBytes)}))
|
||||
}
|
||||
|
||||
eventHandler.Push(event.NewEvent(event.AppError, map[event.Field]string{event.Data: response.Error()}))
|
||||
}
|
||||
|
||||
|
@ -960,7 +837,7 @@ func c_SetContactAttribute(profile_ptr *C.char, profile_len C.int, contact_ptr *
|
|||
// SetContactAttribute provides a wrapper around profile.SetProfileAttribute
|
||||
func SetContactAttribute(profileOnion string, contactHandle string, key string, value string) {
|
||||
profile := application.GetPeer(profileOnion)
|
||||
profile.SetContactAttribute(contactHandle, key, value)
|
||||
profile.SetConversationAttribute(contactHandle, key, value)
|
||||
}
|
||||
|
||||
//export c_SetGroupAttribute
|
||||
|
@ -1181,4 +1058,4 @@ func SetServerAttribute(onion string, key string, val string) {
|
|||
// ***** END Server APIs *****
|
||||
|
||||
// Leave as is, needed by ffi
|
||||
func main() {}
|
||||
//func main() {}
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
echo "Checking code quality (you want to see no output here)"
|
||||
echo ""
|
||||
|
||||
echo "Vetting:"
|
||||
go list ./... | xargs go vet
|
||||
|
||||
echo ""
|
||||
echo "Linting:"
|
||||
|
||||
|
|
|
@ -13,4 +13,5 @@ type Contact struct {
|
|||
IsGroup bool `json:"isGroup"`
|
||||
GroupServer string `json:"groupServer"`
|
||||
IsArchived bool `json:"isArchived"`
|
||||
Identifier int `json:"identifier"`
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"git.openprivacy.ca/cwtch.im/libcwtch-go/features/servers"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
import "cwtch.im/cwtch/event"
|
||||
|
||||
|
@ -61,8 +62,13 @@ func (eh *EventHandler) GetNextEvent() string {
|
|||
select {
|
||||
case e := <-appChan:
|
||||
return eh.handleAppBusEvent(&e)
|
||||
case ev := <-eh.profileEvents:
|
||||
return eh.handleProfileEvent(&ev)
|
||||
default:
|
||||
select {
|
||||
case e := <-appChan:
|
||||
return eh.handleAppBusEvent(&e)
|
||||
case ev := <-eh.profileEvents:
|
||||
return eh.handleProfileEvent(&ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,104 +123,83 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
|
|||
}
|
||||
}
|
||||
|
||||
picVal, ok := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.Picture)
|
||||
if !ok {
|
||||
picVal = ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro))
|
||||
}
|
||||
pic, err := StringToImage(picVal)
|
||||
if err != nil {
|
||||
pic = NewImage(RandomProfileImage(onion), TypeImageDistro)
|
||||
}
|
||||
picPath := GetPicturePath(pic)
|
||||
|
||||
// Set publicly scopes attributes
|
||||
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants2.Picture, picPath)
|
||||
|
||||
online, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline)
|
||||
|
||||
// Name always exists
|
||||
e.Data[constants.Name], _ = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
e.Data[constants2.Picture] = picPath
|
||||
e.Data[constants2.Picture] = RandomProfileImage(onion)
|
||||
e.Data["Online"] = online
|
||||
|
||||
var contacts []Contact
|
||||
var servers []groups.Server
|
||||
for _, contact := range profile.GetContacts() {
|
||||
|
||||
conversations, err := profile.FetchConversations()
|
||||
if err != nil {
|
||||
/// um....
|
||||
return ""
|
||||
}
|
||||
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 profile.GetContact(contact).IsServer() {
|
||||
if conversationInfo.IsServer() {
|
||||
groupHandler, err := groups.ExperimentGate(ReadGlobalSettings().Experiments)
|
||||
if err == nil {
|
||||
servers = append(servers, groupHandler.GetServerInfo(contact, profile))
|
||||
servers = append(servers, groupHandler.GetServerInfo(conversationInfo.Handle, profile))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
contactInfo := profile.GetContact(contact)
|
||||
ph := NewPeerHelper(profile)
|
||||
name := ph.GetNick(contact)
|
||||
cpicPath := ph.GetProfilePic(contact)
|
||||
saveHistory, set := contactInfo.GetAttribute(event.SaveHistoryKey)
|
||||
name,_ := conversationInfo.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
cpicPath := RandomProfileImage(conversationInfo.Handle)
|
||||
saveHistory, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, event.SaveHistoryKey)
|
||||
if !set {
|
||||
saveHistory = event.DeleteHistoryDefault
|
||||
}
|
||||
isArchived, set := contactInfo.GetAttribute(attr.GetLocalScope(constants2.Archived))
|
||||
isArchived, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants2.Archived)
|
||||
if !set {
|
||||
isArchived = event.False
|
||||
}
|
||||
contacts = append(contacts, Contact{
|
||||
Name: name,
|
||||
Onion: contactInfo.Onion,
|
||||
Status: contactInfo.State,
|
||||
Picture: cpicPath,
|
||||
Authorization: string(contactInfo.Authorization),
|
||||
SaveHistory: saveHistory,
|
||||
Messages: contactInfo.Timeline.Len(),
|
||||
Unread: 0,
|
||||
LastMessage: strconv.Itoa(getLastMessageTime(&contactInfo.Timeline)),
|
||||
IsGroup: false,
|
||||
IsArchived: isArchived == event.True,
|
||||
})
|
||||
}
|
||||
|
||||
// We compile and send the groups regardless of the experiment flag, and hide them in the UI
|
||||
for _, groupId := range profile.GetGroups() {
|
||||
group := profile.GetGroup(groupId)
|
||||
|
||||
// Check that the group is cryptographically valid
|
||||
if !group.CheckGroup() {
|
||||
continue
|
||||
state, set := profile.GetPeerState(conversationInfo.Handle)
|
||||
if !set {
|
||||
state = connections.DISCONNECTED
|
||||
}
|
||||
|
||||
ph := NewPeerHelper(profile)
|
||||
cpicPath := ph.GetProfilePic(groupId)
|
||||
|
||||
authorization := model.AuthUnknown
|
||||
if group.Accepted {
|
||||
if conversationInfo.Accepted {
|
||||
authorization = model.AuthApproved
|
||||
}
|
||||
isArchived, set := group.GetAttribute(attr.GetLocalScope(constants2.Archived))
|
||||
if !set {
|
||||
isArchived = event.False
|
||||
|
||||
if acl, exists := conversationInfo.ACL[conversationInfo.Handle]; exists && acl.Blocked {
|
||||
authorization = model.AuthBlocked
|
||||
}
|
||||
// Use the server state when assessing group state
|
||||
state := profile.GetContact(group.GroupServer).State
|
||||
|
||||
groupServer,_ := conversationInfo.GetAttribute(attr.LocalScope, attr.LegacyGroupZone, constants.GroupServer)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
contacts = append(contacts, Contact{
|
||||
Name: ph.GetNick(groupId),
|
||||
Onion: group.GroupID,
|
||||
Status: state,
|
||||
Name: name,
|
||||
Identifier: conversationInfo.ID,
|
||||
Onion: conversationInfo.Handle,
|
||||
Status: connections.ConnectionStateName[state],
|
||||
Picture: cpicPath,
|
||||
Authorization: string(authorization),
|
||||
SaveHistory: event.SaveHistoryConfirmed,
|
||||
Messages: group.Timeline.Len(),
|
||||
SaveHistory: saveHistory,
|
||||
Messages: count,
|
||||
Unread: 0,
|
||||
LastMessage: strconv.Itoa(getLastMessageTime(&group.Timeline)),
|
||||
IsGroup: true,
|
||||
GroupServer: group.GroupServer,
|
||||
LastMessage: strconv.Itoa(getLastMessageTime(lastMessage)),
|
||||
IsGroup: conversationInfo.IsGroup(),
|
||||
GroupServer: groupServer,
|
||||
IsArchived: isArchived == event.True,
|
||||
})
|
||||
}
|
||||
|
@ -239,56 +224,59 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string {
|
|||
if eh.app == nil {
|
||||
log.Errorf("eh.app == nil in handleProfileEvent... this shouldnt happen?")
|
||||
} else {
|
||||
peer := eh.app.GetPeer(ev.Profile)
|
||||
ph := NewPeerHelper(peer)
|
||||
profile := eh.app.GetPeer(ev.Profile)
|
||||
log.Debugf("New Profile Event to Handle: %v", ev)
|
||||
switch ev.Event.EventType {
|
||||
|
||||
/*
|
||||
TODO: still handle this somewhere - network info from plugin Network check
|
||||
case event.NetworkStatus:
|
||||
online, _ := peer.GetAttribute(attr.GetLocalScope(constants.PeerOnline))
|
||||
if e.Data[event.Status] == plugins.NetworkCheckSuccess && online == event.False {
|
||||
peer.SetAttribute(attr.GetLocalScope(constants.PeerOnline), event.True)
|
||||
uiManager.UpdateNetworkStatus(true)
|
||||
// TODO we may have to reinitialize the peer
|
||||
} else if e.Data[event.Status] == plugins.NetworkCheckError && online == event.True {
|
||||
peer.SetAttribute(attr.GetLocalScope(constants.PeerOnline), event.False)
|
||||
uiManager.UpdateNetworkStatus(false)
|
||||
}*/
|
||||
|
||||
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
|
||||
// only needs contact nickname and picture, for displaying on popup notifications
|
||||
ev.Event.Data["Nick"] = ph.GetNick(ev.Event.Data["RemotePeer"])
|
||||
ev.Event.Data["Picture"] = ph.GetProfilePic(ev.Event.Data["RemotePeer"])
|
||||
peer.SetContactAttribute(ev.Event.Data["RemotePeer"], attr.GetLocalScope(constants2.Archived), event.False)
|
||||
ci, err := profile.FetchConversationInfo(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["Nick"],_ = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
ev.Event.Data["Picture"] = RandomProfileImage(ev.Event.Data["RemotePeer"])
|
||||
|
||||
case event.NewMessageFromGroup:
|
||||
// only needs contact nickname and picture, for displaying on popup notifications
|
||||
ev.Event.Data["Nick"] = ph.GetNick(ev.Event.Data[event.GroupID])
|
||||
ev.Event.Data["Picture"] = ph.GetProfilePic(ev.Event.Data[event.GroupID])
|
||||
peer.SetGroupAttribute(ev.Event.Data[event.GroupID], attr.GetLocalScope(constants2.Archived), event.False)
|
||||
ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"])
|
||||
if ci != nil && err == nil {
|
||||
ev.Event.Data["Nick"],_ = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
}
|
||||
ev.Event.Data["Picture"] = RandomProfileImage(ev.Event.Data[event.GroupID])
|
||||
conversationID,_ := strconv.Atoi(ev.Event.Data[event.ConversationID])
|
||||
profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False)
|
||||
case event.PeerAcknowledgement:
|
||||
// No enrichement required
|
||||
ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"])
|
||||
if ci != nil && err == nil {
|
||||
ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID)
|
||||
}
|
||||
case event.PeerCreated:
|
||||
handle := ev.Event.Data[event.RemotePeer]
|
||||
err := EnrichNewPeer(handle, ph, ev)
|
||||
|
||||
ev.Event.Data["unread"] = strconv.Itoa(ph.CountUnread(group.Timeline.GetMessages(), lastRead))
|
||||
ev.Event.Data["picture"] = ph.GetProfilePic(handle)
|
||||
ev.Event.Data["numMessages"] = strconv.Itoa(group.Timeline.Len())
|
||||
ev.Event.Data["nick"] = ph.GetNick(handle)
|
||||
ev.Event.Data["status"] = group.State
|
||||
ev.Event.Data["authorization"] = string(model.AuthApproved)
|
||||
ev.Event.Data["loading"] = "false"
|
||||
ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(&group.Timeline))
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
case event.GroupCreated:
|
||||
// This event should only happen after we have validated the invite, as such the error
|
||||
// condition *should* never happen.
|
||||
|
||||
groupPic := ph.GetProfilePic(ev.Event.Data[event.GroupID])
|
||||
groupPic := RandomGroupImage(ev.Event.Data[event.GroupID])
|
||||
ev.Event.Data["PicturePath"] = groupPic
|
||||
ev.Event.Data["GroupName"] = ph.GetNick(ev.Event.Data[event.GroupID])
|
||||
|
||||
case event.NewGroup:
|
||||
// This event should only happen after we have validated the invite, as such the error
|
||||
// condition *should* never happen.
|
||||
serializedInvite := ev.Event.Data[event.GroupInvite]
|
||||
if invite, err := model.ValidateInvite(serializedInvite); err == nil {
|
||||
groupPic := ph.GetProfilePic(invite.GroupID)
|
||||
groupPic := RandomGroupImage(invite.GroupID)
|
||||
ev.Event.Data["PicturePath"] = groupPic
|
||||
} else {
|
||||
log.Errorf("received a new group event which contained an invalid invite %v. this should never happen and likely means there is a bug in cwtch. Please file a ticket @ https://git.openprivcy.ca/cwtch.im/cwtch", err)
|
||||
|
@ -296,10 +284,10 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string {
|
|||
}
|
||||
case event.PeerStateChange:
|
||||
cxnState := connections.ConnectionStateToType()[ev.Event.Data[event.ConnectionState]]
|
||||
contact := peer.GetContact(ev.Event.Data[event.RemotePeer])
|
||||
contact,_ := profile.FetchConversationInfo(ev.Event.Data[event.RemotePeer])
|
||||
|
||||
if cxnState == connections.AUTHENTICATED && contact == nil {
|
||||
peer.AddContact(ev.Event.Data[event.RemotePeer], ev.Event.Data[event.RemotePeer], model.AuthUnknown)
|
||||
profile.NewContactConversation(ev.Event.Data[event.RemotePeer], model.AccessControl{Read: false, Append: false, Blocked: false}, false)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
@ -308,21 +296,21 @@ func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string {
|
|||
//uiManager.UpdateContactStatus(contact.Onion, int(cxnState), false)
|
||||
if cxnState == connections.AUTHENTICATED {
|
||||
// if known and authed, get vars
|
||||
peer.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
peer.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants2.Picture)
|
||||
profile.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
profile.SendScopedZonedGetValToContact(ev.Event.Data[event.RemotePeer], attr.PublicScope, attr.ProfileZone, constants2.Picture)
|
||||
}
|
||||
}
|
||||
|
||||
case event.NewRetValMessageFromPeer:
|
||||
// auto handled event means the setting is already done, we're just deciding if we need to tell the UI
|
||||
onion := ev.Event.Data[event.RemotePeer]
|
||||
conversationID,_ := strconv.Atoi(ev.Event.Data[event.ConversationID])
|
||||
scope := ev.Event.Data[event.Scope]
|
||||
path := ev.Event.Data[event.Path]
|
||||
//val := ev.Event.Data[event.Data]
|
||||
exists, _ := strconv.ParseBool(ev.Event.Data[event.Exists])
|
||||
|
||||
if exists && attr.IntoScope(scope) == attr.PublicScope {
|
||||
if _, exists := peer.GetContactAttribute(onion, attr.GetLocalScope(path)); exists {
|
||||
zone,path := attr.ParseZone(path)
|
||||
if _, err := profile.GetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(path))); err != nil {
|
||||
// we have a locally set override, don't pass this remote set public scope update to UI
|
||||
return ""
|
||||
}
|
||||
|
@ -361,7 +349,6 @@ func (eh *EventHandler) startHandlingPeer(onion string) {
|
|||
eventBus.Subscribe(event.SendMessageToPeerError, q)
|
||||
eventBus.Subscribe(event.ServerStateChange, q)
|
||||
eventBus.Subscribe(event.PeerStateChange, q)
|
||||
eventBus.Subscribe(event.PeerCreated, q)
|
||||
eventBus.Subscribe(event.NetworkStatus, q)
|
||||
eventBus.Subscribe(event.ChangePasswordSuccess, q)
|
||||
eventBus.Subscribe(event.ChangePasswordError, q)
|
||||
|
@ -395,3 +382,14 @@ func (eh *EventHandler) forwardProfileMessages(onion string, q event.Queue) {
|
|||
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())
|
||||
}
|
251
utils/manager.go
251
utils/manager.go
|
@ -1,251 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"errors"
|
||||
"git.openprivacy.ca/cwtch.im/libcwtch-go/constants"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PeerHelper struct {
|
||||
peer peer.CwtchPeer
|
||||
}
|
||||
|
||||
func NewPeerHelper(profile peer.CwtchPeer) *PeerHelper {
|
||||
return &PeerHelper{profile}
|
||||
}
|
||||
|
||||
func (p *PeerHelper) IsGroup(id string) bool {
|
||||
return len(id) == 32 && !p.IsServer(id)
|
||||
}
|
||||
|
||||
func (p *PeerHelper) IsPeer(id string) bool {
|
||||
return len(id) == 56 && !p.IsServer(id)
|
||||
}
|
||||
|
||||
// Check if the id is associated with a contact with a KeyTypeServerOnion attribute (which indicates that this
|
||||
// is a server, not a regular contact or a group
|
||||
func (p *PeerHelper) IsServer(id string) bool {
|
||||
_, ok := p.peer.GetContactAttribute(id, string(model.KeyTypeServerOnion))
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetTimeline returns a pointer to the timeline associated with the conversation handle or nil if the handle
|
||||
// does not exist (this can happen if the conversation has been deleted)
|
||||
func (p *PeerHelper) GetTimeline(handle string) *model.Timeline {
|
||||
if p.IsServer(handle) {
|
||||
// This should *never* happen
|
||||
log.Errorf("server accessed as contact when getting timeline...")
|
||||
return &model.Timeline{}
|
||||
}
|
||||
// We return a pointer to the timeline to avoid copying, accessing Timeline is thread-safe
|
||||
if p.IsGroup(handle) {
|
||||
group := p.peer.GetGroup(handle)
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
return &group.Timeline
|
||||
}
|
||||
contact := p.peer.GetContact(handle)
|
||||
if contact == nil {
|
||||
return nil
|
||||
}
|
||||
return &contact.Timeline
|
||||
}
|
||||
|
||||
/*
|
||||
func getOrDefault(id, key string, defaultVal string) string {
|
||||
var val string
|
||||
var ok bool
|
||||
if IsGroup(id) {
|
||||
val, ok = the.Peer.GetGroupAttribute(id, key)
|
||||
} else {
|
||||
val, ok = the.Peer.GetContactAttribute(id, key)
|
||||
}
|
||||
if ok {
|
||||
return val
|
||||
} else {
|
||||
return defaultVal
|
||||
}
|
||||
}*/
|
||||
|
||||
func (p *PeerHelper) GetWithSetDefault(id string, key string, defaultVal string) string {
|
||||
var val string
|
||||
var ok bool
|
||||
if p.IsGroup(id) {
|
||||
val, ok = p.peer.GetGroupAttribute(id, key)
|
||||
} else {
|
||||
val, ok = p.peer.GetContactAttribute(id, key)
|
||||
}
|
||||
if !ok {
|
||||
val = defaultVal
|
||||
if p.IsGroup(id) {
|
||||
p.peer.SetGroupAttribute(id, key, defaultVal)
|
||||
} else {
|
||||
p.peer.SetContactAttribute(id, key, defaultVal)
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p *PeerHelper) GetNick(id string) string {
|
||||
if p.IsGroup(id) {
|
||||
nick, exists := p.peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Name))
|
||||
if !exists || nick == "" || nick == id {
|
||||
nick, exists = p.peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Name))
|
||||
if !exists {
|
||||
nick = "[" + id + "]"
|
||||
}
|
||||
}
|
||||
return nick
|
||||
} else {
|
||||
nick, exists := p.peer.GetContactAttribute(id, attr.GetLocalScope(constants.Name))
|
||||
if !exists || nick == "" || nick == id {
|
||||
nick, exists = p.peer.GetContactAttribute(id, attr.GetPeerScope(constants.Name))
|
||||
if !exists {
|
||||
nick = "[" + id + "]"
|
||||
// we do not have a canonical nick for this contact.
|
||||
// re-request if authenticated
|
||||
// TODO: This check probably doesn't belong here...
|
||||
if contact := p.peer.GetContact(id); contact != nil && contact.State == connections.ConnectionStateName[connections.AUTHENTICATED] {
|
||||
p.peer.SendScopedZonedGetValToContact(id, attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nick
|
||||
}
|
||||
}
|
||||
|
||||
// InitLastReadTime checks and gets the Attributable's LastRead time or sets it to now
|
||||
func (p *PeerHelper) InitLastReadTime(id string) time.Time {
|
||||
nowStr, _ := time.Now().MarshalText()
|
||||
lastReadAttr := p.GetWithSetDefault(id, attr.GetLocalScope(constants.LastRead), string(nowStr))
|
||||
var lastRead time.Time
|
||||
lastRead.UnmarshalText([]byte(lastReadAttr))
|
||||
return lastRead
|
||||
}
|
||||
|
||||
// GetProfilePic returns a string path to an image to display for hte given peer/group id
|
||||
func (p *PeerHelper) GetProfilePic(id string) string {
|
||||
if p.IsGroup(id) {
|
||||
if picVal, exists := p.peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Picture)); exists {
|
||||
pic, err := StringToImage(picVal)
|
||||
if err == nil {
|
||||
return GetPicturePath(pic)
|
||||
}
|
||||
}
|
||||
if picVal, exists := p.peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Picture)); exists {
|
||||
pic, err := StringToImage(picVal)
|
||||
if err == nil {
|
||||
return GetPicturePath(pic)
|
||||
}
|
||||
}
|
||||
return GetPicturePath(NewImage(RandomGroupImage(id), TypeImageDistro))
|
||||
|
||||
} else {
|
||||
if picVal, exists := p.peer.GetContactAttribute(id, attr.GetLocalScope(constants.Picture)); exists {
|
||||
pic, err := StringToImage(picVal)
|
||||
if err == nil {
|
||||
return GetPicturePath(pic)
|
||||
}
|
||||
}
|
||||
if picVal, exists := p.peer.GetContactAttribute(id, attr.GetPeerScope(constants.Picture)); exists {
|
||||
pic, err := StringToImage(picVal)
|
||||
if err == nil {
|
||||
return GetPicturePath(pic)
|
||||
}
|
||||
}
|
||||
return RandomProfileImage(id)
|
||||
}
|
||||
}
|
||||
|
||||
// a lot of pics were stored full path + uri. remove all this to the relative path in images/
|
||||
// fix for storing full paths introduced 2019.12
|
||||
func profilePicRelativize(filename string) string {
|
||||
parts := strings.Split(filename, "qml/images")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func GetPicturePath(pic *image) string {
|
||||
switch pic.T {
|
||||
case TypeImageDistro:
|
||||
return profilePicRelativize(pic.Val)
|
||||
default:
|
||||
log.Errorf("Unhandled profile picture type of %v\n", pic.T)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PeerHelper) CountUnread(messages []model.Message, lastRead time.Time) int {
|
||||
count := 0
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Timestamp.After(lastRead) || messages[i].Timestamp.Equal(lastRead) {
|
||||
count++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func getLastMessageTime(tl *model.Timeline) int {
|
||||
if len(tl.Messages) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(tl.Messages[len(tl.Messages)-1].Timestamp.Unix())
|
||||
}
|
||||
|
||||
// EnrichNewPeer populates required data for use by frontend
|
||||
// uiManager.AddContact(onion)
|
||||
// (handle string, displayName string, image string, badge int, status int, authorization string, loading bool, lastMsgTime int)
|
||||
func EnrichNewPeer(handle string, ph *PeerHelper, ev *EventProfileEnvelope) error {
|
||||
if ph.IsGroup(handle) {
|
||||
group := ph.peer.GetGroup(handle)
|
||||
if group != nil {
|
||||
lastRead := ph.InitLastReadTime(group.GroupID)
|
||||
ev.Event.Data["unread"] = strconv.Itoa(ph.CountUnread(group.Timeline.GetMessages(), lastRead))
|
||||
ev.Event.Data["picture"] = ph.GetProfilePic(handle)
|
||||
ev.Event.Data["numMessages"] = strconv.Itoa(group.Timeline.Len())
|
||||
ev.Event.Data["nick"] = ph.GetNick(handle)
|
||||
ev.Event.Data["status"] = group.State
|
||||
ev.Event.Data["authorization"] = string(model.AuthApproved)
|
||||
ev.Event.Data["loading"] = "false"
|
||||
ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(&group.Timeline))
|
||||
}
|
||||
} else if ph.IsPeer(handle) {
|
||||
contact := ph.peer.GetContact(handle)
|
||||
if contact != nil {
|
||||
lastRead := ph.InitLastReadTime(contact.Onion)
|
||||
ev.Event.Data["unread"] = strconv.Itoa(ph.CountUnread(contact.Timeline.GetMessages(), lastRead))
|
||||
ev.Event.Data["numMessages"] = strconv.Itoa(contact.Timeline.Len())
|
||||
ev.Event.Data["picture"] = ph.GetProfilePic(handle)
|
||||
|
||||
ev.Event.Data["nick"] = ph.GetNick(handle)
|
||||
|
||||
// TODO Replace this if with a better flow that separates New Contacts and Peering Updates
|
||||
if contact.State == "" {
|
||||
// Will be disconnected to start
|
||||
ev.Event.Data["status"] = connections.ConnectionStateName[connections.DISCONNECTED]
|
||||
} else {
|
||||
ev.Event.Data["status"] = contact.State
|
||||
}
|
||||
ev.Event.Data["authorization"] = string(contact.Authorization)
|
||||
ev.Event.Data["loading"] = "false"
|
||||
ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(&contact.Timeline))
|
||||
} else {
|
||||
log.Errorf("Failed to find contact: %v", handle)
|
||||
}
|
||||
} else {
|
||||
// could be a server?
|
||||
log.Debugf("sorry, unable to handle AddContact(%v)", handle)
|
||||
return errors.New("not a peer or group")
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue