Updated Cwtch Refactor

This commit is contained in:
Sarah Jamie Lewis 2021-11-17 12:33:51 -08:00
parent 86c5a51b22
commit dcdbf382cb
11 changed files with 185 additions and 796 deletions

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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}
}

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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() {}

View File

@ -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:"

View File

@ -13,4 +13,5 @@ type Contact struct {
IsGroup bool `json:"isGroup"`
GroupServer string `json:"groupServer"`
IsArchived bool `json:"isArchived"`
Identifier int `json:"identifier"`
}

View File

@ -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())
}

View File

@ -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
}