From e674a0cb330a44c25849fa2ed3c307841e8e29fd Mon Sep 17 00:00:00 2001 From: ycc Date: Tue, 28 May 2024 16:47:04 +0200 Subject: [PATCH] peers separate --- client/helpers/call.go | 4 +- client/helpers/invitationAnswerHelper.go | 6 +- client/helpers/invitationCreateHelper.go | 26 +++++--- client/helpers/messageHelper.go | 8 +-- client/helpers/storageHelper.go | 4 +- client/identity.go | 32 ++++++---- client/identity_test.go | 75 +++++++++++------------- client/messagestorage_test.go | 15 +++-- client/peer_test.go | 7 ++- client/peerstorage.go | 10 +++- client/test.id | 56 ++++++++++++++++++ 11 files changed, 164 insertions(+), 79 deletions(-) create mode 100644 client/test.id diff --git a/client/helpers/call.go b/client/helpers/call.go index 89838e4..c547251 100644 --- a/client/helpers/call.go +++ b/client/helpers/call.go @@ -36,8 +36,8 @@ func ReadCallRequestResponseMessage(data []byte, srvuid string) (*meowlib.VideoD return serverMsg.VideoData, "", nil } -func BuildCallMessage(videodata *meowlib.VideoData, srvuid string, peer_idx int, replyToUid string, filelist []string) ([]byte, string, error) { - peer := client.GetConfig().GetIdentity().Peers[peer_idx] +func BuildCallMessage(videodata *meowlib.VideoData, srvuid string, peer_uid string, replyToUid string, filelist []string) ([]byte, string, error) { + peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid) // Creating User message usermessage, err := peer.BuildSimpleUserMessage(nil) diff --git a/client/helpers/invitationAnswerHelper.go b/client/helpers/invitationAnswerHelper.go index 0bfad35..4457ff3 100644 --- a/client/helpers/invitationAnswerHelper.go +++ b/client/helpers/invitationAnswerHelper.go @@ -91,14 +91,16 @@ func InvitationAnswerFile(invitationFile string, nickname string, myNickname str func InvitationAnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) { // find the peer with that invitation id - var peer *client.Peer + /*var peer *client.Peer for i := len(client.GetConfig().GetIdentity().Peers) - 1; i >= 0; i-- { //! to allow self invitation : testing only, findinc the received peer before myself // for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { peer = client.GetConfig().GetIdentity().Peers[i] break } - } + }*/ + peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId) + if peer == nil { // declare a custom go error for no peer found return nil, "InvitationAnswerMessage: loop for peer", errors.New("no peer with that invitation id") diff --git a/client/helpers/invitationCreateHelper.go b/client/helpers/invitationCreateHelper.go index 12e63e8..df81f31 100644 --- a/client/helpers/invitationCreateHelper.go +++ b/client/helpers/invitationCreateHelper.go @@ -73,12 +73,14 @@ func InvitationCreateMessage(invitationId string, invitationServerUid string, ti // lookup for peer with "invitation_id" var myContact *meowlib.ContactCard - for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { + /* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { myContact = client.GetConfig().GetIdentity().Peers[i].GetMyContact() break } - } + }*/ + peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId) + myContact = peer.GetMyContact() // todo handle not found !! // lookup for message server with "invitation_server" invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) //.GetServerByIdx(int(jsoninv["invitation_server"].(float64))) @@ -122,14 +124,20 @@ func InvitationCreateReadResponse(invitationServerUid string, invitationResponse // invitationId: the invitation id of the peer // url: the url of the invitation we got from the server func InvitationSetUrlInfo(invitationId string, url string, expiry int64) { + id := client.GetConfig().GetIdentity() + // lookup for peer with "invitation_id" + peer := id.Peers.GetFromInvitationId(invitationId) + peer.InvitationUrl = url + peer.InvitationExpiry = time.Unix(expiry, 0) + id.Peers.StorePeer(peer) - for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { - if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { - client.GetConfig().GetIdentity().Peers[i].InvitationUrl = url - client.GetConfig().GetIdentity().Peers[i].InvitationExpiry = time.Unix(expiry, 0) - break + /* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { + if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { + client.GetConfig().GetIdentity().Peers[i].InvitationUrl = url + client.GetConfig().GetIdentity().Peers[i].InvitationExpiry = time.Unix(expiry, 0) + break + } } - } - client.GetConfig().GetIdentity().Save() + client.GetConfig().GetIdentity().Save()*/ } diff --git a/client/helpers/messageHelper.go b/client/helpers/messageHelper.go index 77904f6..02c54bf 100644 --- a/client/helpers/messageHelper.go +++ b/client/helpers/messageHelper.go @@ -30,9 +30,9 @@ func messageBuildPostprocess(msg *meowlib.UserMessage, srvuid string, peer *clie return data, "", nil } -func PrepareUserMessage(message string, srvuid string, peer_idx int, replyToUid string, filelist []string) ([]byte, string, error) { +func PrepareUserMessage(message string, srvuid string, peer_uid string, replyToUid string, filelist []string) ([]byte, string, error) { - peer := client.GetConfig().GetIdentity().Peers[peer_idx] + peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid) // Creating User message usermessage, err := peer.BuildSimpleUserMessage([]byte(message)) @@ -51,9 +51,9 @@ func PrepareUserMessage(message string, srvuid string, peer_idx int, replyToUid } -func BuildAckMessage(messageUid string, srvuid string, peer_idx int, received int64, processed int64) ([]byte, string, error) { +func BuildAckMessage(messageUid string, srvuid string, peer_uid string, received int64, processed int64) ([]byte, string, error) { - peer := client.GetConfig().GetIdentity().Peers[peer_idx] + peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid) srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid) if err != nil { return nil, "PrepareServerMessage : LoadServer", err diff --git a/client/helpers/storageHelper.go b/client/helpers/storageHelper.go index ebadade..1a11093 100644 --- a/client/helpers/storageHelper.go +++ b/client/helpers/storageHelper.go @@ -4,9 +4,9 @@ import ( "forge.redroom.link/yves/meowlib/client" ) -func LoadMessagesHistory(peer_id int) ([]client.InternalUserMessage, string, error) { +func LoadMessagesHistory(peer_uid string) ([]client.InternalUserMessage, string, error) { id := client.GetConfig().GetIdentity() - peer := id.Peers[peer_id] + peer := id.Peers.GetFromUid(peer_uid) msgs, err := peer.LoadMessagesHistory(0, 0, 50) if err != nil { return nil, "LoadLastMessages: LoadMessagesHistory", err diff --git a/client/identity.go b/client/identity.go index e76664a..6c288f2 100644 --- a/client/identity.go +++ b/client/identity.go @@ -21,7 +21,7 @@ type Identity struct { Avatars []Avatar `json:"avatars,omitempty"` RootKp meowlib.KeyPair `json:"id_kp,omitempty"` Status string `json:"status,omitempty"` - Peers PeerList `json:"peers,omitempty"` + Peers PeerStorage `json:"peers,omitempty"` HiddenPeers [][]byte `json:"hidden_peers,omitempty"` Personae PeerList `json:"faces,omitempty"` Device meowlib.KeyPair `json:"device,omitempty"` @@ -96,7 +96,7 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerU peer.MyPullServers = MessageServerUids peer.MyName = MyName peer.InvitationMessage = InvitationMessage - id.Peers = append(id.Peers, &peer) + id.Peers.StorePeer(&peer) return &peer, nil } @@ -104,14 +104,15 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerU // Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) { // invitation Id found, this is an answer to an invitation - for _, p := range id.Peers { + /*for _, p := range id.Peers { if p.InvitationId == ReceivedContact.InvitationId { return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage } } // it's an invitation - return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage + return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage*/ + return id.Peers.CheckInvitation(ReceivedContact) } // Answers an invitation, returns the newly created peer including infos to provide a ContactCard @@ -146,14 +147,14 @@ func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageS peer.MyPullServers = MessageServerIdxs peer.MyName = MyName peer.InvitationId = ReceivedContact.InvitationId - id.Peers = append(id.Peers, &peer) + id.Peers.StorePeer(&peer) return &peer } // Finalizes an invitation, returns nil if successful func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error { - for i, p := range id.Peers { + /*for i, p := range id.Peers { if p.InvitationId == ReceivedContact.InvitationId { //id.Peers[i].Name = ReceivedContact.Name id.Peers[i].ContactEncryption = ReceivedContact.EncryptionPublicKey @@ -167,7 +168,9 @@ func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) err return nil } } - return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId) + return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)*/ + + return id.Peers.FinalizeInvitation(ReceivedContact) } // LoadIdentity loads an identity from an encrypted file @@ -225,6 +228,7 @@ func (id *Identity) TryUnlockHidden(password string) error { return errors.New("no peer found") } +/* func (id *Identity) HidePeer(peerIdx int, password string) error { serializedPeer, err := json.Marshal(id.Peers[peerIdx]) if err != nil { @@ -239,13 +243,14 @@ func (id *Identity) HidePeer(peerIdx int, password string) error { // remove clear text peer id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...) return nil -} +}*/ func (id *Identity) generateRandomHiddenStuff() { r := rand.New(rand.NewSource(time.Now().UnixNano())) count := r.Intn(maxHiddenCount) + 1 for i := 1; i < count; i++ { var p Peer + p.Uid = uuid.New().String() p.Name = randomLenString(4, 20) p.MyEncryptionKp = meowlib.NewKeyPair() p.MyIdentity = meowlib.NewKeyPair() @@ -254,9 +259,10 @@ func (id *Identity) generateRandomHiddenStuff() { p.ContactPublicKey = p.MyLookupKp.Public p.ContactEncryption = p.MyIdentity.Public p.ContactLookupKey = p.MyEncryptionKp.Public + p.dbPassword = randomLenString(8, 14) // p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)}) // todo add servers - id.Peers = append(id.Peers, &p) - id.HidePeer(0, randomLenString(8, 14)) + id.Peers.StorePeer(&p) + //id.HidePeer(0, randomLenString(8, 14)) // TODO Add random conversations } } @@ -285,7 +291,11 @@ func (id *Identity) GetRequestJobs() []*RequestsJob { srvs[server.GetServerCard().GetUid()] = &rj } // add ids to the map - for _, peer := range id.Peers { + peers, err := id.Peers.GetPeers() + if err != nil { + return nil + } + for _, peer := range peers { // check if peer inviation is accepted for _, server := range peer.MyPullServers { srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp) diff --git a/client/identity_test.go b/client/identity_test.go index f1db96f..c370e3b 100644 --- a/client/identity_test.go +++ b/client/identity_test.go @@ -7,6 +7,7 @@ import ( "testing" "forge.redroom.link/yves/meowlib" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -32,17 +33,21 @@ func createId() *Identity { if err != nil { log.Fatal("Save failed") } - var p Peer - p.Name = "testName" - p.MyEncryptionKp = meowlib.NewKeyPair() - p.MyIdentity = meowlib.NewKeyPair() - p.MyLookupKp = meowlib.NewKeyPair() - p.Name = "foo" - p.ContactPublicKey = p.MyLookupKp.Public - p.ContactEncryption = p.MyIdentity.Public - p.ContactLookupKey = p.MyEncryptionKp.Public - //p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"}) //todo add servers - id.Peers = append(id.Peers, &p) + for i := range 10 { + var p Peer + p.Uid = uuid.New().String() + p.Name = "testName_" + strconv.Itoa(i) + p.MyEncryptionKp = meowlib.NewKeyPair() + p.MyIdentity = meowlib.NewKeyPair() + p.MyLookupKp = meowlib.NewKeyPair() + p.Name = "foo_" + strconv.Itoa(i) + p.ContactPublicKey = meowlib.NewKeyPair().Public + p.ContactEncryption = meowlib.NewKeyPair().Public + p.ContactLookupKey = meowlib.NewKeyPair().Public + p.MyPullServers = []string{"server1", "server2"} + //p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"}) //todo add servers + id.Peers.StorePeer(&p) + } return id } @@ -72,42 +77,28 @@ func TestLoad(t *testing.T) { } func TestHidePeer(t *testing.T) { - id := createId() - name := id.Peers[0].Name - assert.Equal(t, len(id.Peers), 1) - h := len(id.HiddenPeers) - id.HidePeer(0, "mypassword") - assert.Equal(t, len(id.Peers), 0) - assert.Equal(t, len(id.HiddenPeers), h+1) - id.TryUnlockHidden("mypassword") - assert.Equal(t, len(id.unlockedHiddenPeers), 1) - assert.Equal(t, id.unlockedHiddenPeers[0].Name, name) - if exists("test.id") { - os.Remove("test.id") - } + /* + id := createId() + name := id.Peers[0].Name + assert.Equal(t, len(id.Peers), 1) + h := len(id.HiddenPeers) + id.HidePeer(0, "mypassword") + assert.Equal(t, len(id.Peers), 0) + assert.Equal(t, len(id.HiddenPeers), h+1) + id.TryUnlockHidden("mypassword") + assert.Equal(t, len(id.unlockedHiddenPeers), 1) + assert.Equal(t, id.unlockedHiddenPeers[0].Name, name) + + if exists("test.id") { + os.Remove("test.id") + } + */ } // test GetRequestJobs func TestGetRequestJobs(t *testing.T) { // Create a mock Identity object - id := &Identity{ - Peers: []*Peer{ - { - MyPullServers: []string{"server1", "server2"}, - MyLookupKp: meowlib.NewKeyPair(), - }, - { - MyPullServers: []string{"server3", "server4"}, - MyLookupKp: meowlib.NewKeyPair(), - }, - }, - unlockedHiddenPeers: []*Peer{ - { - MyPullServers: []string{"server5", "server6"}, - MyLookupKp: meowlib.NewKeyPair(), - }, - }, - } + id := createId() id.MessageServers = ServerStorage{ DbFile: "test.db", } diff --git a/client/messagestorage_test.go b/client/messagestorage_test.go index c2a1b5f..215421b 100644 --- a/client/messagestorage_test.go +++ b/client/messagestorage_test.go @@ -14,11 +14,15 @@ func TestStoreMessage(t *testing.T) { id := createId() var um meowlib.UserMessage um.Data = []byte("blabla") - err := StoreMessage(id.Peers[0], &um, []string{}, GetConfig().memoryPassword) + peers, err := id.Peers.GetPeers() if err != nil { log.Fatal(err) } - messages, err := GetMessagesHistory(id.Peers[0], 0, 0, 10, GetConfig().memoryPassword) + err = StoreMessage(peers[0], &um, []string{}, GetConfig().memoryPassword) + if err != nil { + log.Fatal(err) + } + messages, err := GetMessagesHistory(peers[0], 0, 0, 10, GetConfig().memoryPassword) if err != nil { log.Fatal(err) } @@ -42,8 +46,9 @@ func TestStoreMessage(t *testing.T) { func TestManyStoreMessage(t *testing.T) { id := createId() + peers, err := id.Peers.GetPeers() // test with zero messages - messages, err := GetMessagesHistory(id.Peers[0], 0, 0, 10, GetConfig().memoryPassword) + messages, err := GetMessagesHistory(peers[0], 0, 0, 10, GetConfig().memoryPassword) if err != nil { log.Fatal(err) } @@ -51,12 +56,12 @@ func TestManyStoreMessage(t *testing.T) { for i := 1; i < 100; i++ { var um meowlib.UserMessage um.Data = []byte(randomLenString(20, 200)) - err := StoreMessage(id.Peers[0], &um, []string{}, GetConfig().memoryPassword) + err := StoreMessage(peers[0], &um, []string{}, GetConfig().memoryPassword) if err != nil { log.Fatal(err) } } - messages, err = GetMessagesHistory(id.Peers[0], 0, 0, 10, GetConfig().memoryPassword) + messages, err = GetMessagesHistory(peers[0], 0, 0, 10, GetConfig().memoryPassword) if err != nil { log.Fatal(err) } diff --git a/client/peer_test.go b/client/peer_test.go index 43dcc82..d561115 100644 --- a/client/peer_test.go +++ b/client/peer_test.go @@ -4,6 +4,7 @@ import ( "strconv" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -15,9 +16,13 @@ func TestGetFromPublicKey(t *testing.T) { id.Save() for i := 1; i < 10; i++ { var p Peer + p.Uid = uuid.New().String() p.Name = "test" + strconv.Itoa(i) p.ContactPublicKey = "stringToFind" + strconv.Itoa(i) - id.Peers = append(id.Peers, &p) + err := id.Peers.StorePeer(&p) + if err != nil { + t.Fatal("StorePeer failed") + } } p5 := id.Peers.GetFromPublicKey("stringToFind5") assert.Equal(t, p5.Name, "test5") diff --git a/client/peerstorage.go b/client/peerstorage.go index 6a51a2f..a07ccec 100644 --- a/client/peerstorage.go +++ b/client/peerstorage.go @@ -12,6 +12,7 @@ import ( "forge.redroom.link/yves/meowlib" "github.com/dgraph-io/badger" + "github.com/google/uuid" ) type PeerStorage struct { @@ -22,7 +23,12 @@ type PeerStorage struct { // Open the badger database from struct PeerStorage func (ps *PeerStorage) open() error { - + if ps.DbFile == "" { + ps.DbFile = uuid.New().String() + } + if ps.cache == nil { + ps.cache = make(map[string]*Peer) + } opts := badger.DefaultOptions(filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, ps.DbFile)) opts.Logger = nil var err error @@ -55,6 +61,8 @@ func (ps *PeerStorage) StorePeer(peer *Peer) error { } shakey := sha256.Sum256([]byte(peer.Uid)) key := shakey[:] + // add it to cache + ps.cache[peer.Uid] = peer // then store it in the database return ps.db.Update(func(txn *badger.Txn) error { return txn.Set(key, data) diff --git a/client/test.id b/client/test.id new file mode 100644 index 0000000..85c23fe --- /dev/null +++ b/client/test.id @@ -0,0 +1,56 @@ +-----BEGIN PGP MESSAGE----- +Version: GopenPGP 2.7.5 +Comment: https://gopenpgp.org + +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 +oSo1 +=gq+d +-----END PGP MESSAGE----- \ No newline at end of file