diff --git a/Makefile b/Makefile index 117f48a3a54be1a5a4ed6dd976d193e03adfbcdc..c899add28240bd6c3c47544c55a47d4e9c47c8aa 100644 --- a/Makefile +++ b/Makefile @@ -14,11 +14,13 @@ build: update_release: GOFLAGS="" go get -d gitlab.com/elixxir/client@release GOFLAGS="" go get gitlab.com/elixxir/crypto@release + GOFLAGS="" go get gitlab.com/elixxir/primitives@release GOFLAGS="" go get gitlab.com/xx_network/primitives@release update_master: GOFLAGS="" go get -d gitlab.com/elixxir/client@master GOFLAGS="" go get gitlab.com/elixxir/crypto@master + GOFLAGS="" go get gitlab.com/elixxir/primitives@master GOFLAGS="" go get gitlab.com/xx_network/primitives@master binary: diff --git a/go.mod b/go.mod index f47b1d74b356269f59a8bb673978aa1d88591b08..12ed3aa7ac1e7cb2037738036340f579940cbf5d 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/hack-pad/go-indexeddb v0.2.0 github.com/pkg/errors v0.9.1 github.com/spf13/jwalterweatherman v1.1.0 - gitlab.com/elixxir/client v1.5.1-0.20220920212200-25ceacdcbd31 - gitlab.com/elixxir/crypto v0.0.7-0.20220920002307-5541473e9aa7 + gitlab.com/elixxir/client v1.5.1-0.20221004163122-5a4635dce0fa + gitlab.com/elixxir/crypto v0.0.7-0.20221003185354-b091598d2322 + gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 gitlab.com/xx_network/primitives v0.0.4-0.20220809193445-9fc0a5209548 ) @@ -38,10 +39,9 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/blake3 v0.2.3 // indirect gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f // indirect - gitlab.com/elixxir/comms v0.0.4-0.20220913220502-eed192f654bd // indirect + gitlab.com/elixxir/comms v0.0.4-0.20220916185715-f1e9a5eda939 // indirect gitlab.com/elixxir/ekv v0.2.1 // indirect - gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 // indirect - gitlab.com/xx_network/comms v0.0.4-0.20220913215811-c4bf83b27de3 // indirect + gitlab.com/xx_network/comms v0.0.4-0.20220916185248-8a984b8594de // indirect gitlab.com/xx_network/crypto v0.0.5-0.20220913213008-98764f5b3287 // indirect gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect go.uber.org/atomic v1.10.0 // indirect diff --git a/go.sum b/go.sum index 3f19ed257d84bf386fc99877142b9cfd1ba5a2ce..34119701844d9e3c94cd258b9a9c4b1aaea848b5 100644 --- a/go.sum +++ b/go.sum @@ -614,15 +614,15 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/client v1.5.1-0.20220920212200-25ceacdcbd31 h1:+Di3jF1Vdmr+gQGYwJOSKqbpRylTzLVP81fzkdkpegw= -gitlab.com/elixxir/client v1.5.1-0.20220920212200-25ceacdcbd31/go.mod h1:pX1uLFS8v6pNVzJEcfbMUrYPTWLPl8p71ghqW2Xm0Ns= -gitlab.com/elixxir/comms v0.0.4-0.20220913220502-eed192f654bd h1:2nHE7EoptSTBFjCxMeAveKT6urbguCwgg8Jx7XYEVe4= -gitlab.com/elixxir/comms v0.0.4-0.20220913220502-eed192f654bd/go.mod h1:AO6XkMhaHJW8eXlgL5m3UUcJqsSP8F5Wm1GX+wyq/rw= +gitlab.com/elixxir/client v1.5.1-0.20221004163122-5a4635dce0fa h1:sjZ+73Jesh/wU036YbZ5UAGjLIeKCVscf7sQDHMC4DM= +gitlab.com/elixxir/client v1.5.1-0.20221004163122-5a4635dce0fa/go.mod h1:wuTIcLuMnvIGSo8i/Gg/SbYF57bE+CbKPpA1Xbk2AKk= +gitlab.com/elixxir/comms v0.0.4-0.20220916185715-f1e9a5eda939 h1:+VRx2ULHKs040bBhDAOKNCZnbcXxUk3jD9JoKQzQpLk= +gitlab.com/elixxir/comms v0.0.4-0.20220916185715-f1e9a5eda939/go.mod h1:AO6XkMhaHJW8eXlgL5m3UUcJqsSP8F5Wm1GX+wyq/rw= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c= gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA= gitlab.com/elixxir/crypto v0.0.7-0.20220913220142-ab0771bad0af/go.mod h1:QF8SzsrYh9Elip9EUYUDAhPjqO9DGrrrQxYHvn+VXok= -gitlab.com/elixxir/crypto v0.0.7-0.20220920002307-5541473e9aa7 h1:9IsBtL8zcUG86XcfNUVIKcnlL5tyKlyQt1cJ5nogr1U= -gitlab.com/elixxir/crypto v0.0.7-0.20220920002307-5541473e9aa7/go.mod h1:QF8SzsrYh9Elip9EUYUDAhPjqO9DGrrrQxYHvn+VXok= +gitlab.com/elixxir/crypto v0.0.7-0.20221003185354-b091598d2322 h1:8unQE70BDNRXTWUbjOO9d4kWyh19LySlTZo0Jqx0gPE= +gitlab.com/elixxir/crypto v0.0.7-0.20221003185354-b091598d2322/go.mod h1:QF8SzsrYh9Elip9EUYUDAhPjqO9DGrrrQxYHvn+VXok= gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is= gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU= gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg= @@ -634,8 +634,9 @@ gitlab.com/elixxir/primitives v0.0.3-0.20220810173935-592f34a88326/go.mod h1:9Bb gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 h1:/cxxZBP5jTPDpC3zgOx9vV1ojmJyG8pYtkl3IbcewNQ= gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA= gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw= -gitlab.com/xx_network/comms v0.0.4-0.20220913215811-c4bf83b27de3 h1:7mReTvEUVoI5Qpltcmbodc/j6rdPPHDIvenY4ZmWP7o= gitlab.com/xx_network/comms v0.0.4-0.20220913215811-c4bf83b27de3/go.mod h1:E2QKOKyPKLRjLUwMxgZpTKueEsHDEqshfqOHJ54ttxU= +gitlab.com/xx_network/comms v0.0.4-0.20220916185248-8a984b8594de h1:44VKuVgT6X1l+MX8/oNmYORA+pa4nkOWV8hYxi4SCzc= +gitlab.com/xx_network/comms v0.0.4-0.20220916185248-8a984b8594de/go.mod h1:E2QKOKyPKLRjLUwMxgZpTKueEsHDEqshfqOHJ54ttxU= gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE= gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk= gitlab.com/xx_network/crypto v0.0.5-0.20220913213008-98764f5b3287 h1:Jd71F8f/8rieWybMqkxpKKZVVyGkeCNZWZcviGGnQ9A= diff --git a/indexedDb/implementation.go b/indexedDb/implementation.go index e88499d22e452013be2c39de73f057a74ec163fd..1d8d804074140da708672192358375f6a3ebe4dd 100644 --- a/indexedDb/implementation.go +++ b/indexedDb/implementation.go @@ -13,12 +13,14 @@ import ( "context" "encoding/base64" "encoding/json" + "sync" + "syscall/js" + "time" + "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/xxdk-wasm/utils" - "syscall/js" - "time" "gitlab.com/elixxir/client/channels" "gitlab.com/elixxir/client/cmix/rounds" @@ -35,7 +37,9 @@ const dbTimeout = time.Second // system passed an object that adheres to in order to get events on the // channel. type wasmModel struct { - db *idb.Database + db *idb.Database + receivedMessageCB MessageReceivedCallback + updateMux sync.Mutex } // newContext builds a context for database operations. @@ -49,7 +53,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { // Build object newChannel := Channel{ - Id: channel.ReceptionID.Marshal(), + ID: channel.ReceptionID.Marshal(), Name: channel.Name, Description: channel.Description, } @@ -83,7 +87,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { } // Perform the operation - _, err = store.Add(channelObj) + _, err = store.Put(channelObj) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, "Unable to Add Channel: %+v", err)) @@ -145,16 +149,32 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) { // It may be called multiple times on the same message; it is incumbent on the // user of the API to filter such called by message ID. func (w *wasmModel) ReceiveMessage(channelID *id.ID, - messageID cryptoChannel.MessageID, senderUsername string, text string, - timestamp time.Time, lease time.Duration, _ rounds.Round, - status channels.SentStatus) { - parentErr := errors.New("failed to ReceiveMessage") + messageID cryptoChannel.MessageID, nickname, text string, + identity cryptoChannel.Identity, timestamp time.Time, lease time.Duration, + round rounds.Round, mType channels.MessageType, + status channels.SentStatus) uint64 { + + msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), nil, + nickname, text, identity, timestamp, lease, round.ID, mType, status) + + // Attempt a lookup on the MessageID if it is non-zero to find an existing + // entry for it. This occurs any time a sender receives their own message + // from the mixnet. + if !messageID.Equals(cryptoChannel.MessageID{}) { + uuid, err := w.msgIDLookup(messageID) + if err != nil { + // message is already in the database, no insert necessary + return uuid + } + } - err := w.receiveHelper(buildMessage(channelID.Marshal(), messageID.Bytes(), - nil, senderUsername, text, timestamp, lease, status)) + uuid, err := w.receiveHelper(msgToInsert) if err != nil { - jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + jww.ERROR.Printf("Failed to receiver message: %+v", err) } + + go w.receivedMessageCB(uuid, channelID, false) + return uuid } // ReceiveReply is called whenever a message is received that is a reply on a @@ -165,15 +185,32 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, // the initial message. As a result, it may be important to buffer replies. func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID cryptoChannel.MessageID, replyTo cryptoChannel.MessageID, - senderUsername string, text string, timestamp time.Time, - lease time.Duration, _ rounds.Round, status channels.SentStatus) { - parentErr := errors.New("failed to ReceiveReply") + nickname, text string, identity cryptoChannel.Identity, timestamp time.Time, + lease time.Duration, round rounds.Round, mType channels.MessageType, + status channels.SentStatus) uint64 { + + msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), + replyTo.Bytes(), nickname, text, identity, timestamp, lease, round.ID, + mType, status) + + // Attempt a lookup on the MessageID if it is non-zero to find an existing + // entry for it. This occurs any time a sender receives their own message + // from the mixnet. + if !messageID.Equals(cryptoChannel.MessageID{}) { + uuid, err := w.msgIDLookup(messageID) + if err != nil { + // message is already in the database, no insert necessary + return uuid + } + } + + uuid, err := w.receiveHelper(msgToInsert) - err := w.receiveHelper(buildMessage(channelID.Marshal(), messageID.Bytes(), - replyTo.Bytes(), senderUsername, text, timestamp, lease, status)) if err != nil { - jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + jww.ERROR.Printf("Failed to receive reply: %+v", err) } + go w.receivedMessageCB(uuid, channelID, false) + return uuid } // ReceiveReaction is called whenever a reaction to a message is received on a @@ -184,26 +221,50 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, // the initial message. As a result, it may be important to buffer reactions. func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, - senderUsername string, reaction string, timestamp time.Time, - lease time.Duration, _ rounds.Round, status channels.SentStatus) { - parentErr := errors.New("failed to ReceiveReaction") + nickname, reaction string, identity cryptoChannel.Identity, + timestamp time.Time, lease time.Duration, round rounds.Round, + mType channels.MessageType, status channels.SentStatus) uint64 { + + msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), + reactionTo.Bytes(), nickname, reaction, identity, timestamp, lease, + round.ID, mType, status) + + // Attempt a lookup on the MessageID if it is non-zero to find + // an existing entry for it. This occurs any time a sender + // receives their own message from the mixnet. + if !messageID.Equals(cryptoChannel.MessageID{}) { + uuid, err := w.msgIDLookup(messageID) + if err != nil { + // message is already in the database, no insert necessary + return uuid + } + } - err := w.receiveHelper(buildMessage(channelID.Marshal(), messageID.Bytes(), - reactionTo.Bytes(), senderUsername, reaction, timestamp, lease, status)) + uuid, err := w.receiveHelper(msgToInsert) if err != nil { - jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + jww.ERROR.Printf("Failed to receive reaction: %+v", err) } + go w.receivedMessageCB(uuid, channelID, false) + return uuid } // UpdateSentStatus is called whenever the [channels.SentStatus] of a message -// has changed. +// has changed. At this point the message ID goes from empty/unknown to +// populated. +// // TODO: Potential race condition due to separate get/update operations. -func (w *wasmModel) UpdateSentStatus(messageID cryptoChannel.MessageID, - status channels.SentStatus) { +func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status channels.SentStatus) { parentErr := errors.New("failed to UpdateSentStatus") + // FIXME: this is a bit of race condition without the mux. + // This should be done via the transactions (i.e., make a + // special version of receiveHelper) + w.updateMux.Lock() + defer w.updateMux.Unlock() + // Convert messageID to the key generated by json.Marshal - key := js.ValueOf(base64.StdEncoding.EncodeToString(messageID[:])) + key := js.ValueOf(uuid) // Use the key to get the existing Message currentMsg, err := w.get(messageStoreName, key) @@ -218,59 +279,95 @@ func (w *wasmModel) UpdateSentStatus(messageID cryptoChannel.MessageID, return } newMessage.Status = uint8(status) + if !messageID.Equals(cryptoChannel.MessageID{}) { + newMessage.MessageID = messageID.Bytes() + } + + if round.ID == 0 { + newMessage.Round = uint64(round.ID) + } + + if !timestamp.Equal(time.Time{}) { + newMessage.Timestamp = timestamp + } // Store the updated Message - err = w.receiveHelper(newMessage) + _, err = w.receiveHelper(newMessage) if err != nil { jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) } + channelID := &id.ID{} + copy(channelID[:], newMessage.ChannelID) + go w.receivedMessageCB(uuid, channelID, true) } // buildMessage is a private helper that converts typical [channels.EventModel] // inputs into a basic Message structure for insertion into storage. -func buildMessage(channelID, messageID, parentId []byte, senderUsername, - text string, timestamp time.Time, lease time.Duration, +// NOTE: ID is not set inside this function because we want to use the +// autoincrement key by default. If you are trying to overwrite +// an existing message, then you need to set it manually +// yourself. +func buildMessage(channelID, messageID, parentID []byte, nickname, text string, + identity cryptoChannel.Identity, timestamp time.Time, lease time.Duration, + round id.Round, mType channels.MessageType, status channels.SentStatus) *Message { return &Message{ - Id: messageID, - SenderUsername: senderUsername, - ChannelId: channelID, - ParentMessageId: parentId, + MessageID: messageID, + Nickname: nickname, + ChannelID: channelID, + ParentMessageID: parentID, Timestamp: timestamp, Lease: lease, Status: uint8(status), Hidden: false, Pinned: false, Text: text, + Type: uint16(mType), + Round: uint64(round), + // User Identity Info + Pubkey: identity.PubKey, + Codename: identity.Codename, + Color: identity.Color, + Extension: identity.Extension, + CodesetVersion: identity.CodesetVersion, } } // receiveHelper is a private helper for receiving any sort of message. -func (w *wasmModel) receiveHelper(newMessage *Message) error { +func (w *wasmModel) receiveHelper(newMessage *Message) (uint64, + error) { // Convert to jsObject newMessageJson, err := json.Marshal(newMessage) if err != nil { - return errors.Errorf("Unable to marshal Message: %+v", err) + return 0, errors.Errorf("Unable to marshal Message: %+v", err) } messageObj, err := utils.JsonToJS(newMessageJson) if err != nil { - return errors.Errorf("Unable to marshal Message: %+v", err) + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + + // NOTE: This is weird, but correct. When the "ID" field is 0, we + // unset it from the JSValue so that it is auto-populated and + // incremented. + if newMessage.ID == 0 { + messageObj.JSValue().Delete("id") } // Prepare the Transaction txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName) if err != nil { - return errors.Errorf("Unable to create Transaction: %+v", err) + return 0, errors.Errorf("Unable to create Transaction: %+v", + err) } store, err := txn.ObjectStore(messageStoreName) if err != nil { - return errors.Errorf("Unable to get ObjectStore: %+v", err) + return 0, errors.Errorf("Unable to get ObjectStore: %+v", err) } // Perform the upsert (put) operation - _, err = store.Put(messageObj) + addReq, err := store.Put(messageObj) if err != nil { - return errors.Errorf("Unable to upsert Message: %+v", err) + return 0, errors.Errorf("Unable to upsert Message: %+v", err) } // Wait for the operation to return @@ -278,11 +375,15 @@ func (w *wasmModel) receiveHelper(newMessage *Message) error { err = txn.Await(ctx) cancel() if err != nil { - return errors.Errorf("Upserting Message failed: %+v", err) + return 0, errors.Errorf("Upserting Message failed: %+v", err) } + res, _ := addReq.Result() + uuid := uint64(res.Int()) jww.DEBUG.Printf( - "Successfully stored message from %s", newMessage.SenderUsername) - return nil + "Successfully stored message from %s, id %d", + newMessage.Codename, uuid) + + return uuid, nil } // get is a generic private helper for getting values from the given @@ -324,6 +425,56 @@ func (w *wasmModel) get(objectStoreName string, key js.Value) (string, error) { return resultStr, nil } +func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64, + error) { + parentErr := errors.Errorf("failed to get %s/%s", messageStoreName, + messageID) + + // Prepare the Transaction + txn, err := w.db.Transaction(idb.TransactionReadOnly, messageStoreName) + if err != nil { + return 0, errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(messageStoreName) + if err != nil { + return 0, errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + idx, err := store.Index(messageStoreMessageIndex) + if err != nil { + return 0, errors.WithMessagef(parentErr, + "Unable to get index: %+v", err) + } + + msgIDStr := base64.StdEncoding.EncodeToString(messageID.Bytes()) + + keyReq, err := idx.Get(js.ValueOf(msgIDStr)) + if err != nil { + return 0, errors.WithMessagef(parentErr, + "Unable to get keyReq: %+v", err) + } + // Wait for the operation to return + ctx, cancel := newContext() + keyObj, err := keyReq.Await(ctx) + cancel() + if err != nil { + return 0, errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %+v", err) + } + + // Process result into string + resultStr := utils.JsToJson(keyObj) + jww.DEBUG.Printf("Index lookup of %s/%s/%s: %s", messageStoreName, + messageStoreMessageIndex, msgIDStr, resultStr) + + uuid := uint64(0) + if !keyObj.IsUndefined() { + uuid = uint64(keyObj.Get("id").Int()) + } + return uuid, nil +} + // dump returns the given [idb.ObjectStore] contents to string slice for // debugging purposes. func (w *wasmModel) dump(objectStoreName string) ([]string, error) { @@ -349,16 +500,17 @@ func (w *wasmModel) dump(objectStoreName string) ([]string, error) { jww.DEBUG.Printf("%s values:", objectStoreName) results := make([]string, 0) ctx, cancel := newContext() - err = cursorRequest.Iter(ctx, func(cursor *idb.CursorWithValue) error { - value, err := cursor.Value() - if err != nil { - return err - } - valueStr := utils.JsToJson(value) - results = append(results, valueStr) - jww.DEBUG.Printf("- %v", valueStr) - return nil - }) + err = cursorRequest.Iter(ctx, + func(cursor *idb.CursorWithValue) error { + value, err := cursor.Value() + if err != nil { + return err + } + valueStr := utils.JsToJson(value) + results = append(results, valueStr) + jww.DEBUG.Printf("- %v", valueStr) + return nil + }) cancel() if err != nil { return nil, errors.WithMessagef(parentErr, diff --git a/indexedDb/implementation_test.go b/indexedDb/implementation_test.go index a4d5d0bd72b7cdb441dfbad87cfdf7d95ce4215d..f47117f7171ce9851b05a5017745c3b4651370c8 100644 --- a/indexedDb/implementation_test.go +++ b/indexedDb/implementation_test.go @@ -11,14 +11,17 @@ package indexedDb import ( "encoding/json" + "fmt" + "os" + "testing" + "time" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/channels" + "gitlab.com/elixxir/client/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" "gitlab.com/elixxir/crypto/channel" "gitlab.com/xx_network/primitives/id" - "os" - "testing" - "time" ) func TestMain(m *testing.M) { @@ -26,19 +29,24 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func dummyCallback(uint64, *id.ID, bool) {} + // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. func TestWasmModel_UpdateSentStatus(t *testing.T) { testString := "test" - testMsgId := channel.MakeMessageID([]byte(testString)) - eventModel, err := newWasmModel(testString) + testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + eventModel, err := newWASMModel(testString, dummyCallback) if err != nil { t.Fatalf("%+v", err) } + cid := channel.Identity{} + // Store a test message - testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), - nil, testString, testString, time.Now(), time.Second, channels.Sent) - err = eventModel.receiveHelper(testMsg) + testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, + testString, testString, cid, time.Now(), time.Second, 0, 0, + channels.Sent) + uuid, err := eventModel.receiveHelper(testMsg) if err != nil { t.Fatalf("%+v", err) } @@ -54,7 +62,8 @@ func TestWasmModel_UpdateSentStatus(t *testing.T) { // Update the sentStatus expectedStatus := channels.Failed - eventModel.UpdateSentStatus(testMsgId, expectedStatus) + eventModel.UpdateSentStatus(uuid, testMsgId, time.Now(), + rounds.Round{ID: 8675309}, expectedStatus) // Check the resulting status results, err = eventModel.dump(messageStoreName) @@ -74,14 +83,14 @@ func TestWasmModel_UpdateSentStatus(t *testing.T) { } // Make sure other fields didn't change - if resultMsg.SenderUsername != testString { - t.Fatalf("Unexpected SenderUsername: %v", resultMsg.SenderUsername) + if resultMsg.Nickname != testString { + t.Fatalf("Unexpected Nickname: %v", resultMsg.Nickname) } } // Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths. func TestWasmModel_JoinChannel_LeaveChannel(t *testing.T) { - eventModel, err := newWasmModel("test") + eventModel, err := newWASMModel("test", dummyCallback) if err != nil { t.Fatalf("%+v", err) } @@ -116,3 +125,45 @@ func TestWasmModel_JoinChannel_LeaveChannel(t *testing.T) { t.Fatalf("Expected 1 channels to exist") } } + +// Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. +func TestWasmModel_UUIDTest(t *testing.T) { + testString := "testHello" + eventModel, err := newWASMModel(testString, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + cid := channel.Identity{ + Codename: "codename123", + PubKey: []byte{8, 6, 7, 5}, + Color: "#FFFFFF", + Extension: "gif", + CodesetVersion: 0, + } + + uuids := make([]uint64, 10) + + for i := 0; i < 10; i++ { + // Store a test message + channelID := id.NewIdFromBytes([]byte(testString), t) + msgID := channel.MessageID{} + copy(msgID[:], testString+fmt.Sprintf("%d", i)) + rnd := rounds.Round{ID: id.Round(42)} + uuid := eventModel.ReceiveMessage(channelID, msgID, + "test", testString+fmt.Sprintf("%d", i), cid, time.Now(), + time.Hour, rnd, 0, channels.Sent) + uuids[i] = uuid + } + + _, _ = eventModel.dump(messageStoreName) + + for i := 0; i < 10; i++ { + for j := i + 1; j < 10; j++ { + if uuids[i] == uuids[j] { + t.Fatalf("uuid failed: %d[%d] == %d[%d]", + uuids[i], i, uuids[j], j) + } + } + } +} diff --git a/indexedDb/init.go b/indexedDb/init.go index 515505c7bad04b0f81fbe2e621f66b4801280014..9f107ba0cb695e42b41f8d133f261a0703c58ce9 100644 --- a/indexedDb/init.go +++ b/indexedDb/init.go @@ -10,31 +10,52 @@ package indexedDb import ( + "syscall/js" + "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "syscall/js" "gitlab.com/elixxir/client/channels" + "gitlab.com/xx_network/primitives/id" ) const ( - // databaseSuffix is the suffix to be appended to the name of the database. - databaseSuffix = "_messenger" + // databaseSuffix is the suffix to be appended to the name of + // the database. + databaseSuffix = "_speakeasy" - // currentVersion is the current version of the IndexDb runtime. Used for - // migration purposes. + // currentVersion is the current version of the IndexDb + // runtime. Used for migration purposes. currentVersion uint = 1 ) -// NewWasmEventModel returns a [channels.EventModel] backed by a wasmModel. -func NewWasmEventModel(username string) (channels.EventModel, error) { - databaseName := username + databaseSuffix - return newWasmModel(databaseName) +// MessageReceivedCallback is called any time a message is received or updated +// update is true if the row is old and was edited +type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool) + +// NewWASMEventModelBuilder returns an EventModelBuilder which allows +// the channel manager to define the path but the callback is the same +// across the board. +func NewWASMEventModelBuilder( + cb MessageReceivedCallback) channels.EventModelBuilder { + fn := func(path string) (channels.EventModel, error) { + return NewWASMEventModel(path, cb) + } + return fn } -// newWasmModel creates the given [idb.Database] and returns a wasmModel. -func newWasmModel(databaseName string) (*wasmModel, error) { +// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. +// The name should be a base64 encoding of the users public key. +func NewWASMEventModel(path string, cb MessageReceivedCallback) ( + channels.EventModel, error) { + databaseName := path + databaseSuffix + return newWASMModel(databaseName, cb) +} + +// newWASMModel creates the given [idb.Database] and returns a wasmModel. +func newWASMModel(databaseName string, cb MessageReceivedCallback) ( + *wasmModel, error) { // Attempt to open database object ctx, cancel := newContext() defer cancel() @@ -59,7 +80,7 @@ func newWasmModel(databaseName string) (*wasmModel, error) { // Wait for database open to finish db, err := openRequest.Await(ctx) - return &wasmModel{db: db}, err + return &wasmModel{db: db, receivedMessageCB: cb}, err } // v1Upgrade performs the v0 -> v1 database upgrade. @@ -69,7 +90,7 @@ func newWasmModel(databaseName string) (*wasmModel, error) { func v1Upgrade(db *idb.Database) error { storeOpts := idb.ObjectStoreOptions{ KeyPath: js.ValueOf(pkeyName), - AutoIncrement: false, + AutoIncrement: true, } indexOpts := idb.IndexOptions{ Unique: false, @@ -81,6 +102,11 @@ func v1Upgrade(db *idb.Database) error { if err != nil { return err } + _, err = messageStore.CreateIndex(messageStoreMessageIndex, + js.ValueOf(messageStoreMessage), indexOpts) + if err != nil { + return err + } _, err = messageStore.CreateIndex(messageStoreChannelIndex, js.ValueOf(messageStoreChannel), indexOpts) if err != nil { diff --git a/indexedDb/model.go b/indexedDb/model.go index c46bad409ca43a8fb8276c249c3dcb3876dd1457..7204468bfc14b71ce5b7af6caf9a22329592735c 100644 --- a/indexedDb/model.go +++ b/indexedDb/model.go @@ -22,12 +22,15 @@ const ( channelsStoreName = "channels" // Message index names. + messageStoreMessageIndex = "message_id_index" messageStoreChannelIndex = "channel_id_index" messageStoreParentIndex = "parent_message_id_index" messageStoreTimestampIndex = "timestamp_index" messageStorePinnedIndex = "pinned_index" + messageStorePubkeyIndex = "pubkey_index" // Message keyPath names (must match json struct tags). + messageStoreMessage = "message_id" messageStoreChannel = "channel_id" messageStoreParent = "parent_message_id" messageStoreTimestamp = "timestamp" @@ -39,24 +42,41 @@ const ( // A Message belongs to one Channel. // // A Message may belong to one Message (Parent). +// +// A Message belongs to one User (cryptographic identity). +// The user's nickname can change each message, but the rest does not. We +// still duplicate all of it for each entry to simplify code for now. type Message struct { - Id []byte `json:"id"` // Matches pkeyName - SenderUsername string `json:"sender_username"` - ChannelId []byte `json:"channel_id"` // Index - ParentMessageId []byte `json:"parent_message_id"` // Index + ID uint64 `json:"id"` // Matches pkeyName + Nickname string `json:"nickname"` + MessageID []byte `json:"message_id"` // Index + ChannelID []byte `json:"channel_id"` // Index + ParentMessageID []byte `json:"parent_message_id"` // Index Timestamp time.Time `json:"timestamp"` // Index Lease time.Duration `json:"lease"` Status uint8 `json:"status"` Hidden bool `json:"hidden"` Pinned bool `json:"pinned"` // Index Text string `json:"text"` + Type uint16 `json:"type"` + Round uint64 `json:"round"` + + // User cryptographic Identity struct -- could be pulled out + Pubkey []byte `json:"pubkey"` // Index + // Honorific string `json:"honorific"` + // Adjective string `json:"adjective"` + // Noun string `json:"noun"` + Codename string `json:"codename"` + Color string `json:"color"` + Extension string `json:"extension"` + CodesetVersion uint8 `json:"codeset_version"` } // Channel defines the IndexedDb representation of a single Channel. // // A Channel has many Message. type Channel struct { - Id []byte `json:"id"` // Matches pkeyName + ID []byte `json:"id"` // Matches pkeyName Name string `json:"name"` Description string `json:"description"` } diff --git a/main.go b/main.go index 53347e1fcec7d7e8552e7c824472ee50176d391d..a7042228bd1a49304f7bccaae7c4c973755a359c 100644 --- a/main.go +++ b/main.go @@ -42,16 +42,21 @@ func main() { js.Global().Set("ResumeBackup", js.FuncOf(wasm.ResumeBackup)) // wasm/channels.go + js.Global().Set("GenerateChannelIdentity", + js.FuncOf(wasm.GenerateChannelIdentity)) + js.Global().Set("GetPublicChannelIdentity", + js.FuncOf(wasm.GetPublicChannelIdentity)) + js.Global().Set("GetPublicChannelIdentityFromPrivate", + js.FuncOf(wasm.GetPublicChannelIdentityFromPrivate)) js.Global().Set("NewChannelsManager", js.FuncOf(wasm.NewChannelsManager)) + js.Global().Set("LoadChannelsManager", js.FuncOf(wasm.LoadChannelsManager)) js.Global().Set("NewChannelsManagerWithIndexedDb", js.FuncOf(wasm.NewChannelsManagerWithIndexedDb)) - js.Global().Set("NewChannelsManagerWithIndexedDbDummyNameService", - js.FuncOf(wasm.NewChannelsManagerWithIndexedDbDummyNameService)) - js.Global().Set("NewChannelsManagerDummyNameService", - js.FuncOf(wasm.NewChannelsManagerDummyNameService)) - js.Global().Set("GenerateChannel", - js.FuncOf(wasm.GenerateChannel)) + js.Global().Set("LoadChannelsManagerWithIndexedDb", + js.FuncOf(wasm.LoadChannelsManagerWithIndexedDb)) + js.Global().Set("GenerateChannel", js.FuncOf(wasm.GenerateChannel)) js.Global().Set("GetChannelInfo", js.FuncOf(wasm.GetChannelInfo)) + js.Global().Set("IsNicknameValid", js.FuncOf(wasm.IsNicknameValid)) // wasm/cmix.go js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix)) diff --git a/wasm/authenticatedConnection.go b/wasm/authenticatedConnection.go index 8b2d03f263b1d60277ee5709d7437c6d5c4b5583..c18f48d062b918b682a77188b951e3fa16d77d08 100644 --- a/wasm/authenticatedConnection.go +++ b/wasm/authenticatedConnection.go @@ -68,10 +68,11 @@ func (ac *AuthenticatedConnection) GetId(js.Value, []js.Value) interface{} { // [Cmix.WaitForRoundResult] to see if the send succeeded (Uint8Array). // - Rejected with an error if sending fails. func (ac *AuthenticatedConnection) SendE2E(_ js.Value, args []js.Value) interface{} { - payload := utils.CopyBytesToGo(args[2]) + mt := args[0].Int() + payload := utils.CopyBytesToGo(args[1]) promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - sendReport, err := ac.api.SendE2E(args[0].Int(), payload) + sendReport, err := ac.api.SendE2E(mt, payload) if err != nil { reject(utils.JsTrace(err)) } else { @@ -135,12 +136,13 @@ func (ac *AuthenticatedConnection) RegisterListener( // - Resolves to a Javascript representation of the [Connection] object. // - Rejected with an error if loading the parameters or connecting fails. func (c *Cmix) ConnectWithAuthentication(_ js.Value, args []js.Value) interface{} { + e2eID := args[0].Int() recipientContact := utils.CopyBytesToGo(args[1]) e2eParamsJSON := utils.CopyBytesToGo(args[2]) promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { ac, err := c.api.ConnectWithAuthentication( - args[0].Int(), recipientContact, e2eParamsJSON) + e2eID, recipientContact, e2eParamsJSON) if err != nil { reject(utils.JsTrace(err)) } else { diff --git a/wasm/channels.go b/wasm/channels.go index 9c685776864215898e7721f440926e6c1d02ba27..b8e58b14c22c2195fe2875edd26aa835ad498791 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -10,10 +10,12 @@ package wasm import ( + "gitlab.com/xx_network/primitives/id" + "syscall/js" + "gitlab.com/elixxir/client/bindings" "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/utils" - "syscall/js" ) //////////////////////////////////////////////////////////////////////////////// @@ -44,6 +46,11 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]interface{} "SendMessage": js.FuncOf(cm.SendMessage), "SendReply": js.FuncOf(cm.SendReply), "SendReaction": js.FuncOf(cm.SendReaction), + "GetIdentity": js.FuncOf(cm.GetIdentity), + "GetStorageTag": js.FuncOf(cm.GetStorageTag), + "SetNickname": js.FuncOf(cm.SetNickname), + "DeleteNickname": js.FuncOf(cm.DeleteNickname), + "GetNickname": js.FuncOf(cm.GetNickname), // Channel Receiving Logic and Callback Registration "RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler), @@ -61,19 +68,114 @@ func (ch *ChannelsManager) GetID(js.Value, []js.Value) interface{} { return ch.api.GetID() } -// NewChannelsManager constructs a [ChannelsManager]. +// GenerateChannelIdentity creates a new private channel identity +// ([channel.PrivateIdentity]). The public component can be retrieved as JSON +// via [GetPublicChannelIdentityFromPrivate]. +// +// Parameters: +// - args[0] - ID of [Cmix] object in tracker (int). +// +// Returns: +// - JSON of [channel.PrivateIdentity] (Uint8Array). +// - Throws a TypeError if generating the identity fails. +func GenerateChannelIdentity(_ js.Value, args []js.Value) interface{} { + pi, err := bindings.GenerateChannelIdentity(args[0].Int()) + if err != nil { + utils.Throw(utils.TypeError, err) + } + + return utils.CopyBytesToJS(pi) +} + +// GetPublicChannelIdentity constructs a public identity ([channel.Identity]) +// from a bytes version and returns it JSON marshaled. +// +// Parameters: +// - args[0] - Bytes of the public identity ([channel.Identity]) (Uint8Array). +// +// Returns: +// - JSON of the constructed [channel.Identity] (Uint8Array). +// - Throws a TypeError if unmarshalling the bytes or marshalling the identity +// fails. +func GetPublicChannelIdentity(_ js.Value, args []js.Value) interface{} { + marshaledPublic := utils.CopyBytesToGo(args[0]) + pi, err := bindings.GetPublicChannelIdentity(marshaledPublic) + if err != nil { + utils.Throw(utils.TypeError, err) + } + + return utils.CopyBytesToJS(pi) +} + +// GetPublicChannelIdentityFromPrivate returns the public identity +// ([channel.Identity]) contained in the given private identity +// ([channel.PrivateIdentity]). +// +// Parameters: +// - args[0] - Bytes of the private identity +// (channel.PrivateIdentity]) (Uint8Array). +// +// Returns: +// - JSON of the public identity ([channel.Identity]) (Uint8Array). +// - Throws a TypeError if unmarshalling the bytes or marshalling the identity +// fails. +func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) interface{} { + marshaledPrivate := utils.CopyBytesToGo(args[0]) + identity, err := bindings.GetPublicChannelIdentityFromPrivate( + marshaledPrivate) + if err != nil { + utils.Throw(utils.TypeError, err) + } + + return utils.CopyBytesToJS(identity) +} + +// eventModelBuilder adheres to the [bindings.EventModelBuilder] interface. +type eventModelBuilder struct { + build func(args ...interface{}) js.Value +} + +// Build initializes and returns the event model. It wraps a Javascript object +// that has all the methods in [bindings.EventModel] to make it adhere to the Go +// interface [bindings.EventModel]. +func (emb *eventModelBuilder) Build(path string) bindings.EventModel { + emJs := emb.build(path) + return &eventModel{ + joinChannel: utils.WrapCB(emJs, "JoinChannel"), + leaveChannel: utils.WrapCB(emJs, "LeaveChannel"), + receiveMessage: utils.WrapCB(emJs, "ReceiveMessage"), + receiveReply: utils.WrapCB(emJs, "ReceiveReply"), + receiveReaction: utils.WrapCB(emJs, "ReceiveReaction"), + updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"), + } +} + +// NewChannelsManager creates a new [ChannelsManager] from a new private +// identity ([channel.PrivateIdentity]). +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadChannelsManager], passing in the +// storage tag retrieved by [ChannelsManager.GetStorageTag]. // // Parameters: -// - args[0] - ID of [E2e] object in tracker (int). This can be retrieved using -// [E2e.GetID]. -// - args[1] - ID of [UserDiscovery] object in tracker (int). This can be -// retrieved using [UserDiscovery.GetID]. +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// using [Cmix.GetID]. +// - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is +// generated by [GenerateChannelIdentity] (Uint8Array). +// - args[2] - A function that initialises and returns a Javascript object that +// matches the [bindings.EventModel] interface. The function must match the +// Build function in [bindings.EventModelBuilder]. // // Returns: -// - Javascript representation of the [bindings.ChannelsManager] object. -// - Throws a TypeError if logging in fails. +// - Javascript representation of the [ChannelsManager] object. +// - Throws a TypeError if creating the manager fails. func NewChannelsManager(_ js.Value, args []js.Value) interface{} { - cm, err := bindings.NewChannelsManager(args[0].Int(), args[1].Int()) + privateIdentity := utils.CopyBytesToGo(args[1]) + + em := &eventModelBuilder{args[2].Invoke} + + cm, err := bindings.NewChannelsManager(args[0].Int(), privateIdentity, em) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -82,63 +184,76 @@ func NewChannelsManager(_ js.Value, args []js.Value) interface{} { return newChannelsManagerJS(cm) } -// NewChannelsManagerWithIndexedDb constructs a [ChannelsManager] using an -// indexedDb backend. +// LoadChannelsManager loads an existing [ChannelsManager]. +// +// This is for loading a manager for an identity that has already been created. +// The channel manager should have previously been created with +// [NewChannelsManager] and the storage is retrievable with +// [ChannelsManager.GetStorageTag]. // // Parameters: -// - args[0] - ID of [E2e] object in tracker (int). This can be retrieved using -// [E2e.GetID]. -// - args[1] - ID of [UserDiscovery] object in tracker (int). This can be -// retrieved using [UserDiscovery.GetID]. -// - args[2] - username (string). +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// using [Cmix.GetID]. +// - args[1] - The storage tag associated with the previously created channel +// manager and retrieved with [ChannelsManager.GetStorageTag] (string). +// - args[2] - A function that initialises and returns a Javascript object that +// matches the [bindings.EventModel] interface. The function must match the +// Build function in [bindings.EventModelBuilder]. // -// Returns a promise: -// - Resolves to a Javascript representation of the [bindings.ChannelsManager] -// object. -// - Rejected with an error if loading indexedDb or the manager fails. -func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) interface{} { - promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - em, err := indexedDb.NewWasmEventModel(args[2].String()) - if err != nil { - reject(utils.JsTrace(err)) - } - - cm, err := bindings.NewChannelsManagerGoEventModel( - args[0].Int(), args[1].Int(), em) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(newChannelsManagerJS(cm)) - } +// Returns: +// - Javascript representation of the [ChannelsManager] object. +// - Throws a TypeError if loading the manager fails. +func LoadChannelsManager(_ js.Value, args []js.Value) interface{} { + em := &eventModelBuilder{args[2].Invoke} + cm, err := bindings.LoadChannelsManager(args[0].Int(), args[1].String(), em) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil } - return utils.CreatePromise(promiseFn) + return newChannelsManagerJS(cm) } -// NewChannelsManagerWithIndexedDbDummyNameService constructs a -// [ChannelsManager] using an indexedDb backend and a dummy name server instead -// of UD. +// NewChannelsManagerWithIndexedDb creates a new [ChannelsManager] from a new +// private identity ([channel.PrivateIdentity]) and using indexedDb as a backend +// to manage the event model. +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadChannelsManagerWithIndexedDb], passing +// in the storage tag retrieved by [ChannelsManager.GetStorageTag]. // // This function initialises an indexedDb database. // // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved // using [Cmix.GetID]. -// - args[1] - Username (string). +// - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is +// generated by [GenerateChannelIdentity] (Uint8Array). +// - args[2] - Function that takes in the same parameters as +// [indexedDb.MessageReceivedCallback]. On the Javascript side, the uuid is +// returned as an int and the channelID as a Uint8Array. The row in the +// database that was updated can be found using the UUID. The channel ID is +// provided so that the recipient can filter if they want to the processes +// the update now or not. An "update" bool is present which tells you if +// the row is new or if it is an edited old row // // Returns a promise: -// - Resolves to a Javascript representation of the [bindings.ChannelsManager] -// object. +// - Resolves to a Javascript representation of the [ChannelsManager] object. // - Rejected with an error if loading indexedDb or the manager fails. -func NewChannelsManagerWithIndexedDbDummyNameService(_ js.Value, args []js.Value) interface{} { - promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - em, err := indexedDb.NewWasmEventModel(args[1].String()) - if err != nil { - reject(utils.JsTrace(err)) - } +func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) interface{} { + cmixID := args[0].Int() + privateIdentity := utils.CopyBytesToGo(args[1]) + + fn := func(uuid uint64, channelID *id.ID, update bool) { + args[2].Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) + } - cm, err := bindings.NewChannelsManagerGoEventModelDummyNameService( - args[0].Int(), args[1].String(), em) + model := indexedDb.NewWASMEventModelBuilder(fn) + + promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { + cm, err := bindings.NewChannelsManagerGoEventModel( + cmixID, privateIdentity, model) if err != nil { reject(utils.JsTrace(err)) } else { @@ -149,55 +264,66 @@ func NewChannelsManagerWithIndexedDbDummyNameService(_ js.Value, args []js.Value return utils.CreatePromise(promiseFn) } -// NewChannelsManagerDummyNameService constructs a [ChannelsManager] -// using a Javascript event model backend and a dummy name server instead of UD. +// LoadChannelsManagerWithIndexedDb loads an existing [ChannelsManager] using +// an existing indexedDb database as a backend to manage the event model. +// +// This is for loading a manager for an identity that has already been created. +// The channel manager should have previously been created with +// [NewChannelsManagerWithIndexedDb] and the storage is retrievable with +// [ChannelsManager.GetStorageTag]. // // Parameters: -// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved // using [Cmix.GetID]. -// - args[1] - Username (string). -// - args[2] - Javascript object that matches the [bindings.EventModel] -// interface. +// - args[1] - The storage tag associated with the previously created channel +// manager and retrieved with [ChannelsManager.GetStorageTag] (string). +// - args[2] - Function that takes in the same parameters as +// [indexedDb.MessageReceivedCallback]. On the Javascript side, the uuid is +// returned as an int and the channelID as a Uint8Array. The row in the +// database that was updated can be found using the UUID. The channel ID is +// provided so that the recipient can filter if they want to the processes +// the update now or not. An "update" bool is present which tells you if +// the row is new or if it is an edited old row // -// Returns: -// - Javascript representation of the [bindings.ChannelsManager] object. -// - Throws a TypeError if initialising indexedDb or created the new channel -// manager fails. -func NewChannelsManagerDummyNameService(_ js.Value, args []js.Value) interface{} { - em := &eventModel{ - joinChannel: utils.WrapCB(args[2], "JoinChannel"), - leaveChannel: utils.WrapCB(args[2], "LeaveChannel"), - receiveMessage: utils.WrapCB(args[2], "ReceiveMessage"), - receiveReply: utils.WrapCB(args[2], "ReceiveReply"), - receiveReaction: utils.WrapCB(args[2], "ReceiveReaction"), - updateSentStatus: utils.WrapCB(args[2], "UpdateSentStatus"), +// Returns a promise: +// - Resolves to a Javascript representation of the [ChannelsManager] object. +// - Rejected with an error if loading indexedDb or the manager fails. +func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) interface{} { + cmixID := args[0].Int() + storageTag := args[1].String() + + fn := func(uuid uint64, channelID *id.ID, updated bool) { + args[2].Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated) } - cm, err := bindings.NewChannelsManagerGoEventModelDummyNameService( - args[0].Int(), args[1].String(), bindings.NewEventModel(em)) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil + model := indexedDb.NewWASMEventModelBuilder(fn) + + promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { + cm, err := bindings.LoadChannelsManagerGoEventModel( + cmixID, storageTag, model) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(newChannelsManagerJS(cm)) + } } - return newChannelsManagerJS(cm) + return utils.CreatePromise(promiseFn) } -// GenerateChannel is used to create a channel. This makes a new channel of -// which you are the admin. It is only for making new channels, not joining -// existing ones. +// GenerateChannel is used to create a channel a new channel of which you are +// the admin. It is only for making new channels, not joining existing ones. // // It returns a pretty print of the channel and the private key. // -// The name cannot be longer that ____ characters. -// -// The description cannot be longer than ___ and can only use ______ characters. +// The name cannot be longer that __ characters. The description cannot be +// longer than __ and can only use ______ characters. // // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). // - args[1] - The name of the new channel. The name cannot be longer than __ -// characters and must contain only __ characters. It cannot be changed once -// a channel is created (string). +// characters and must contain only _____ characters. It cannot be changed +// once a channel is created. (string). // - args[2] - The description of a channel. The description cannot be longer // than __ characters and must contain only __ characters. It cannot be // changed once a channel is created (string). @@ -455,15 +581,15 @@ func (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) interface{} return utils.CreatePromise(promiseFn) } -// SendReply is used to send a formatted message over a channel. -// Due to the underlying encoding using compression, it isn't possible to define -// the largest payload that can be sent, but it will always be possible to send -// a payload of 766 bytes at minimum. +// SendReply is used to send a formatted message over a channel. Due to the +// underlying encoding using compression, it isn't possible to define the +// largest payload that can be sent, but it will always be possible to send a +// payload of 766 bytes at minimum. // -// If the message ID the reply is sent to does not exist, then the other side -// will post the message as a normal message and not a reply. -// The message will auto delete validUntil after the round it is sent in, -// lasting forever if ValidForever is used. +// If the message ID the reply is sent to is nonexistent, the other side will +// post the message as a normal message and not a reply. The message will auto +// delete validUntil after the round it is sent in, lasting forever if +// [channels.ValidForever] is used. // // Parameters: // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). @@ -544,6 +670,106 @@ func (ch *ChannelsManager) SendReaction(_ js.Value, args []js.Value) interface{} return utils.CreatePromise(promiseFn) } +// GetIdentity returns the marshaled public identity ([channel.Identity]) that +// the channel is using. +// +// Returns: +// - JSON of the [channel.Identity] (Uint8Array). +// - Throws TypeError if marshalling the identity fails. +func (ch *ChannelsManager) GetIdentity(js.Value, []js.Value) interface{} { + i, err := ch.api.GetIdentity() + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(i) +} + +// GetStorageTag returns the storage tag needed to reload the manager. +// +// Returns: +// - Storage tag (string). +func (ch *ChannelsManager) GetStorageTag(js.Value, []js.Value) interface{} { + return ch.api.GetStorageTag() +} + +// SetNickname sets the nickname for a given channel. The nickname must be valid +// according to [IsNicknameValid]. +// +// Parameters: +// - args[0] - The nickname to set (string). +// - args[1] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - Throws TypeError if unmarshalling the ID fails or the nickname is invalid. +func (ch *ChannelsManager) SetNickname(_ js.Value, args []js.Value) interface{} { + err := ch.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nil +} + +// DeleteNickname deletes the nickname for a given channel. +// +// Parameters: +// - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - Throws TypeError if deleting the nickname fails. +func (ch *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) interface{} { + err := ch.api.DeleteNickname(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nil +} + +// GetNickname returns the nickname set for a given channel. Returns an error if +// there is no nickname set. +// +// Parameters: +// - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - The nickname (string). +// - Throws TypeError if the channel has no nickname set. +func (ch *ChannelsManager) GetNickname(_ js.Value, args []js.Value) interface{} { + nickname, err := ch.api.GetNickname(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nickname +} + +// IsNicknameValid checks if a nickname is valid. +// +// Rules: +// 1. A nickname must not be longer than 24 characters. +// 2. A nickname must not be shorter than 1 character. +// +// Parameters: +// - args[0] - Nickname to check (string). +// +// Returns: +// - A Javascript Error object if the nickname is invalid with the reason why. +// - Null if the nickname is valid. +func IsNicknameValid(_ js.Value, args []js.Value) interface{} { + err := bindings.IsNicknameValid(args[0].String()) + if err != nil { + return utils.JsError(err) + } + + return nil +} + //////////////////////////////////////////////////////////////////////////////// // Channel Receiving Logic and Callback Registration // //////////////////////////////////////////////////////////////////////////////// @@ -560,10 +786,16 @@ type channelMessageReceptionCallback struct { // - receivedChannelMessageReport - Returns the JSON of // [bindings.ReceivedChannelMessageReport] (Uint8Array). // - err - Returns an error on failure (Error). +// +// Returns: +// - It must return a unique UUID for the message that it can be referenced by +// later (int). func (cmrCB *channelMessageReceptionCallback) Callback( - receivedChannelMessageReport []byte, err error) { - cmrCB.callback(utils.CopyBytesToJS(receivedChannelMessageReport), - utils.JsTrace(err)) + receivedChannelMessageReport []byte, err error) int { + uuid := cmrCB.callback( + utils.CopyBytesToJS(receivedChannelMessageReport), utils.JsTrace(err)) + + return uuid.Int() } // RegisterReceiveHandler is used to register handlers for non-default message @@ -635,23 +867,33 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). // - messageID - The bytes of the [channel.MessageID] of the received message // (Uint8Array). -// - senderUsername - The username of the sender of the message (string). +// - nickname - The nickname of the sender of the message (string). // - text - The content of the message (string). +// - identity - JSON of the sender's public ([channel.Identity]) (Uint8Array). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). // - lease - The number of nanoseconds that the message is valid for (int). // - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). // - status - The [channels.SentStatus] of the message (int). // // Statuses will be enumerated as such: // Sent = 0 // Delivered = 1 // Failed = 2 -func (em *eventModel) ReceiveMessage(channelID, messageID []byte, - senderUsername, text string, timestamp, lease, roundId, status int64) { - em.receiveMessage(utils.CopyBytesToJS(channelID), - utils.CopyBytesToJS(messageID), - senderUsername, text, timestamp, lease, roundId, status) +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [eventModel.UpdateSentStatus]. +func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, + text string, identity []byte, timestamp, lease, roundId, msgType, + status int64) int64 { + uuid := em.receiveMessage(utils.CopyBytesToJS(channelID), + utils.CopyBytesToJS(messageID), nickname, text, + utils.CopyBytesToJS(identity), + timestamp, lease, roundId, msgType, status) + + return int64(uuid.Int()) } // ReceiveReply is called whenever a message is received that is a reply on a @@ -669,21 +911,31 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, // (Uint8Array). // - senderUsername - The username of the sender of the message (string). // - text - The content of the message (string). +// - identity - JSON of the sender's public ([channel.Identity]) (Uint8Array). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). // - lease - The number of nanoseconds that the message is valid for (int). // - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). // - status - The [channels.SentStatus] of the message (int). // // Statuses will be enumerated as such: // Sent = 0 // Delivered = 1 // Failed = 2 +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, - senderUsername, text string, timestamp, lease, roundId, status int64) { - em.receiveReply(utils.CopyBytesToJS(channelID), + senderUsername, text string, identity []byte, timestamp, lease, roundId, + msgType, status int64) int64 { + uuid := em.receiveReply(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo), - senderUsername, text, timestamp, lease, roundId, status) + senderUsername, text, utils.CopyBytesToJS(identity), + timestamp, lease, roundId, msgType, status) + + return int64(uuid.Int()) } // ReceiveReaction is called whenever a reaction to a message is received on a @@ -701,35 +953,51 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // (Uint8Array). // - senderUsername - The username of the sender of the message (string). // - reaction - The contents of the reaction message (string). +// - identity - JSON of the sender's public ([channel.Identity]) (Uint8Array). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). // - lease - The number of nanoseconds that the message is valid for (int). // - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). // - status - The [channels.SentStatus] of the message (int). // // Statuses will be enumerated as such: // Sent = 0 // Delivered = 1 // Failed = 2 +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte, - senderUsername, reaction string, timestamp, lease, roundId, status int64) { - em.receiveReaction(utils.CopyBytesToJS(channelID), + senderUsername, reaction string, identity []byte, timestamp, lease, roundId, + msgType, status int64) int64 { + uuid := em.receiveReaction(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo), - senderUsername, reaction, timestamp, lease, roundId, status) + senderUsername, reaction, utils.CopyBytesToJS(identity), + timestamp, lease, roundId, msgType, status) + + return int64(uuid.Int()) } // UpdateSentStatus is called whenever the sent status of a message has // changed. // // Parameters: +// - uuid - The unique identifier for the message (int). // - messageID - The bytes of the [channel.MessageID] of the received message // (Uint8Array). +// - timestamp - Time the message was received; represented as nanoseconds +// since unix epoch (int). +// - roundId - The ID of the round that the message was received on (int). // - status - The [channels.SentStatus] of the message (int). // // Statuses will be enumerated as such: // Sent = 0 // Delivered = 1 // Failed = 2 -func (em *eventModel) UpdateSentStatus(messageID []byte, status int64) { - em.updateSentStatus(utils.CopyBytesToJS(messageID), status) +func (em *eventModel) UpdateSentStatus( + uuid int64, messageID []byte, timestamp, roundID, status int64) { + em.updateSentStatus( + uuid, utils.CopyBytesToJS(messageID), timestamp, roundID, status) } diff --git a/wasm/connect.go b/wasm/connect.go index e3bc13313df46d53856c2cb0f1468f4369cec013..cd3cce95d47094d7fb6edb093e093ad9129b9cad 100644 --- a/wasm/connect.go +++ b/wasm/connect.go @@ -61,11 +61,12 @@ func (c *Connection) GetId(js.Value, []js.Value) interface{} { // - Resolves to a Javascript representation of the [Connection] object. // - Rejected with an error if loading the parameters or connecting fails. func (c *Cmix) Connect(_ js.Value, args []js.Value) interface{} { + e2eID := args[0].Int() recipientContact := utils.CopyBytesToGo(args[1]) e2eParamsJSON := utils.CopyBytesToGo(args[2]) promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - api, err := c.api.Connect(args[0].Int(), recipientContact, e2eParamsJSON) + api, err := c.api.Connect(e2eID, recipientContact, e2eParamsJSON) if err != nil { reject(utils.JsTrace(err)) } else { @@ -92,10 +93,11 @@ func (c *Cmix) Connect(_ js.Value, args []js.Value) interface{} { // into [Cmix.WaitForRoundResult] to see if the send succeeded (Uint8Array). // - Rejected with an error if sending fails. func (c *Connection) SendE2E(_ js.Value, args []js.Value) interface{} { + e2eID := args[0].Int() payload := utils.CopyBytesToGo(args[1]) promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - sendReport, err := c.api.SendE2E(args[0].Int(), payload) + sendReport, err := c.api.SendE2E(e2eID, payload) if err != nil { reject(utils.JsTrace(err)) } else { diff --git a/wasm/e2eHandler.go b/wasm/e2eHandler.go index 1929b3539ba721e1b0157a194cecf4964a238c83..556f9699da670662d1a5294c7d7407d02e79304c 100644 --- a/wasm/e2eHandler.go +++ b/wasm/e2eHandler.go @@ -170,13 +170,13 @@ func (e *E2e) RemoveService(_ js.Value, args []js.Value) interface{} { // into [Cmix.WaitForRoundResult] to see if the send succeeded (Uint8Array). // - Rejected with an error if sending fails. func (e *E2e) SendE2E(_ js.Value, args []js.Value) interface{} { + mt := args[0].Int() recipientId := utils.CopyBytesToGo(args[1]) payload := utils.CopyBytesToGo(args[2]) e2eParams := utils.CopyBytesToGo(args[3]) promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - sendReport, err := e.api.SendE2E( - args[0].Int(), recipientId, payload, e2eParams) + sendReport, err := e.api.SendE2E(mt, recipientId, payload, e2eParams) if err != nil { reject(utils.JsTrace(err)) } else { diff --git a/wasm/follow.go b/wasm/follow.go index f794aa606d8396ced9ce570958c989c01aec75e8..627250636d990de545a9ed0b2b98e1afad752116 100644 --- a/wasm/follow.go +++ b/wasm/follow.go @@ -91,8 +91,9 @@ func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) interface{} { // - A promise that resolves if the network is healthy and rejects if the // network is not healthy. func (c *Cmix) WaitForNetwork(_ js.Value, args []js.Value) interface{} { + timeoutMS := args[0].Int() promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - if c.api.WaitForNetwork(args[0].Int()) { + if c.api.WaitForNetwork(timeoutMS) { resolve() } else { reject() diff --git a/wasm/group.go b/wasm/group.go index 31c031af2f42160e03f8cd4f9349d9f10b12a430..ebe3b6aa38ad719407a1f3aaaefee687a9488b7d 100644 --- a/wasm/group.go +++ b/wasm/group.go @@ -113,8 +113,9 @@ func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) interface{} { // into [Cmix.WaitForRoundResult] to see if the send succeeded (Uint8Array). // - Rejected with an error if resending the request fails. func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) interface{} { + groupId := utils.CopyBytesToGo(args[0]) promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - sendReport, err := g.api.ResendRequest(utils.CopyBytesToGo(args[0])) + sendReport, err := g.api.ResendRequest(groupId) if err != nil { reject(utils.JsTrace(err)) } else { @@ -181,9 +182,10 @@ func (g *GroupChat) LeaveGroup(_ js.Value, args []js.Value) interface{} { func (g *GroupChat) Send(_ js.Value, args []js.Value) interface{} { groupId := utils.CopyBytesToGo(args[0]) message := utils.CopyBytesToGo(args[1]) + tag := args[2].String() promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - sendReport, err := g.api.Send(groupId, message, args[2].String()) + sendReport, err := g.api.Send(groupId, message, tag) if err != nil { reject(utils.JsTrace(err)) } else { diff --git a/wasm/ndf.go b/wasm/ndf.go index 0d99bc9e58f4680a232d86d29956c51d5be5a85c..8e84254b3c0b017bb37d14fa1e6993a724d3adcc 100644 --- a/wasm/ndf.go +++ b/wasm/ndf.go @@ -28,9 +28,11 @@ import ( // - Resolves to the JSON of the NDF ([ndf.NetworkDefinition]) (Uint8Array). // - Rejected with an error if downloading fails. func DownloadAndVerifySignedNdfWithUrl(_ js.Value, args []js.Value) interface{} { + url := args[0].String() + cert := args[1].String() + promiseFn := func(resolve, reject func(args ...interface{}) js.Value) { - ndf, err := bindings.DownloadAndVerifySignedNdfWithUrl( - args[0].String(), args[1].String()) + ndf, err := bindings.DownloadAndVerifySignedNdfWithUrl(url, cert) if err != nil { reject(utils.JsTrace(err)) } else { diff --git a/wasm_test.go b/wasm_test.go index d256a93a4d72b0396f9cbda758fa06ab9605dc1e..c17d1df49b02f5ea433b862ddbda430d50314618 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -39,9 +39,9 @@ func TestPublicFunctions(t *testing.T) { // These functions are used internally by the WASM bindings but are not // exposed - "NewEventModel": {}, - "NewChannelsManagerGoEventModel": {}, - "NewChannelsManagerGoEventModelDummyNameService": {}, + "NewEventModel": {}, + "NewChannelsManagerGoEventModel": {}, + "LoadChannelsManagerGoEventModel": {}, } wasmFuncs := getPublicFunctions("wasm", t) bindingsFuncs := getPublicFunctions(