diff --git a/indexedDb/channels/implementation.go b/indexedDb/channels/implementation.go index 875682329d393031c8b89401d376374c6659e538..bcd957f13863d47e2ce493f6c00b5e5bfd4775b4 100644 --- a/indexedDb/channels/implementation.go +++ b/indexedDb/channels/implementation.go @@ -167,7 +167,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { // user of the API to filter such called by message ID. func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID cryptoChannel.MessageID, nickname, text string, - pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus) uint64 { textBytes := []byte(text) @@ -203,7 +203,7 @@ 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, - nickname, text string, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, + nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus) uint64 { textBytes := []byte(text) @@ -239,7 +239,7 @@ 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, - nickname, reaction string, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, + nickname, reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus) uint64 { textBytes := []byte(reaction) @@ -332,7 +332,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, // 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 string, - text []byte, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, timestamp time.Time, + text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round id.Round, mType channels.MessageType, status channels.SentStatus) *Message { return &Message{ diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go index 27f33649eac4d02514a09b6e2117fb148c6b764e..9f17457013d26125f05d411d29b09afff4cd16c3 100644 --- a/indexedDb/channels/implementation_test.go +++ b/indexedDb/channels/implementation_test.go @@ -60,7 +60,7 @@ func TestWasmModel_msgIDLookup(t *testing.T) { } testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) _, err = eventModel.receiveHelper(testMsg, false) if err != nil { @@ -100,7 +100,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { // Store a test message testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) uuid, err := eventModel.receiveHelper(testMsg, false) if err != nil { @@ -225,7 +225,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) { 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), []byte{8, 6, 7, 5}, 0, + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent) uuids[i] = uuid } @@ -270,7 +270,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { channelID := id.NewIdFromBytes([]byte(testString), t) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", - testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent) uuids[i] = uuid } @@ -326,7 +326,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1}) eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr, - []byte{8, 6, 7, 5}, 0, netTime.Now(), time.Second, + []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, rounds.Round{ID: id.Round(0)}, 0, channels.Sent) } @@ -400,7 +400,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { // First message insert should succeed testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) _, err = eventModel.receiveHelper(testMsg, false) if err != nil { @@ -423,7 +423,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { // Now insert a message with a different message ID from the first testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2}) testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) primaryKey, err := eventModel.receiveHelper(testMsg, false) if err != nil { diff --git a/indexedDb/channels/model.go b/indexedDb/channels/model.go index 629c9f43353ade792f024f88aba769c3c89f22c0..078d6bd67f7c43d9e50373b7801f69937cfce2ac 100644 --- a/indexedDb/channels/model.go +++ b/indexedDb/channels/model.go @@ -61,7 +61,7 @@ type Message struct { // User cryptographic Identity struct -- could be pulled out Pubkey []byte `json:"pubkey"` - DmToken []byte `json:"dm_token"` + DmToken uint32 `json:"dm_token"` CodesetVersion uint8 `json:"codeset_version"` } diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..5f2361ac1363714d3f8d899231ab7f746a8ad1b9 --- /dev/null +++ b/indexedDb/dm/implementation.go @@ -0,0 +1,382 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "crypto/ed25519" + "encoding/json" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/cmix/rounds" + "gitlab.com/elixxir/client/v4/dm" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/xx_network/primitives/id" + "strings" + "sync" + "syscall/js" + "time" + + "github.com/hack-pad/go-indexeddb/idb" + cryptoChannel "gitlab.com/elixxir/crypto/channel" +) + +// wasmModel implements [dm.Receiver] interface, which uses the channels +// system passed an object that adheres to in order to get events on the +// channel. +type wasmModel struct { + db *idb.Database + cipher cryptoChannel.Cipher + receivedMessageCB MessageReceivedCallback + updateMux sync.Mutex +} + +// joinConversation is used for joining new conversations. +func (w *wasmModel) joinConversation(nickname string, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8) error { + parentErr := errors.New("failed to joinConversation") + + // Build object + newConvo := Conversation{ + Pubkey: pubKey, + Nickname: nickname, + Token: dmToken, + CodesetVersion: codeset, + Blocked: false, + } + + // Convert to jsObject + newConvoJson, err := json.Marshal(&newConvo) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to marshal Conversation: %+v", err) + } + convoObj, err := utils.JsonToJS(newConvoJson) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to marshal Conversation: %+v", err) + } + + _, err = indexedDb.Put(w.db, conversationStoreName, convoObj) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to put Conversation: %+v", err) + } + return nil +} + +// buildMessage is a private helper that converts typical [dm.Receiver] +// inputs into a basic Message structure for insertion into storage. +// +// 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(messageID, parentID []byte, text []byte, + pubKey ed25519.PublicKey, timestamp time.Time, round id.Round, + mType dm.MessageType, status dm.Status) *Message { + return &Message{ + MessageID: messageID, + ConversationPubKey: pubKey, + ParentMessageID: parentID, + Timestamp: timestamp, + Status: uint8(status), + Text: text, + Type: uint16(mType), + Round: uint64(round), + } +} + +func (w *wasmModel) Receive(messageID dm.MessageID, nickname string, text []byte, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, + round rounds.Round, mType dm.MessageType, status dm.Status) uint64 { + parentErr := errors.New("failed to Receive") + var err error + + // If there is no extant Conversation, create one. + if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + if w.cipher != nil { + text, err = w.cipher.Encrypt(text) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, text, + pubKey, timestamp, round.ID, mType, status) + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveText(messageID dm.MessageID, nickname, text string, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, + round rounds.Round, status dm.Status) uint64 { + parentErr := errors.New("failed to ReceiveText") + var err error + + // If there is no extant Conversation, create one. + if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + textBytes := []byte(text) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.TextType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveReply(messageID dm.MessageID, reactionTo dm.MessageID, + nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, round rounds.Round, status dm.Status) uint64 { + parentErr := errors.New("failed to ReceiveReply") + var err error + + // If there is no extant Conversation, create one. + if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + textBytes := []byte(text) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), textBytes, + pubKey, timestamp, round.ID, dm.TextType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveReaction(messageID dm.MessageID, reactionTo dm.MessageID, + nickname, reaction string, pubKey ed25519.PublicKey, dmToken uint32, + codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { + parentErr := errors.New("failed to ReceiveText") + var err error + + // If there is no extant Conversation, create one. + if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + textBytes := []byte(reaction) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.ReactionType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) UpdateSentStatus(uuid uint64, + messageID dm.MessageID, timestamp time.Time, round rounds.Round, + status dm.Status) { + 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(uuid) + + // Use the key to get the existing Message + currentMsg, err := indexedDb.Get(w.db, messageStoreName, key) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get message: %+v", err)) + return + } + + // Extract the existing Message and update the Status + newMessage := &Message{} + err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Could not JSON unmarshal message: %+v", err)) + return + } + + newMessage.Status = uint8(status) + if !messageID.Equals(dm.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, true) + if err != nil { + jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + } + go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, true) +} + +// receiveHelper is a private helper for receiving any sort of message. +func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, + error) { + // Convert to jsObject + newMessageJson, err := json.Marshal(newMessage) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + messageObj, err := utils.JsonToJS(newMessageJson) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + + // Unset the primaryKey for inserts so that it can be auto-populated and + // incremented + if !isUpdate { + messageObj.Delete("id") + } + + // Store message to database + addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj) + if err != nil { + return 0, errors.Errorf("Unable to put Message: %+v", err) + } + res, err := addReq.Result() + if err != nil { + return 0, errors.Errorf("Unable to get Message result: %+v", err) + } + + // NOTE: Sometimes the insert fails to return an error but hits a duplicate + // insert, so this fallthrough returns the UUID entry in that case. + if res.IsUndefined() { + msgID := cryptoChannel.MessageID{} + copy(msgID[:], newMessage.MessageID) + uuid, errLookup := w.msgIDLookup(msgID) + if uuid != 0 && errLookup == nil { + return uuid, nil + } + return 0, errors.Errorf("uuid lookup failure: %+v", err) + } + uuid := uint64(res.Int()) + jww.DEBUG.Printf("Successfully stored message %d", uuid) + + return uuid, nil +} + +// msgIDLookup gets the UUID of the Message with the given messageID. +func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64, + error) { + resultObj, err := indexedDb.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal())) + if err != nil { + return 0, err + } + + uuid := uint64(0) + if !resultObj.IsUndefined() { + uuid = uint64(resultObj.Get("id").Int()) + } + return uuid, nil +} diff --git a/indexedDb/dm/init.go b/indexedDb/dm/init.go new file mode 100644 index 0000000000000000000000000000000000000000..d2c3ba1dbb2d6306a39ffc8f5f0b4e6f0159dc73 --- /dev/null +++ b/indexedDb/dm/init.go @@ -0,0 +1,181 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "crypto/ed25519" + "github.com/hack-pad/go-indexeddb/idb" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/dm" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/storage" + "syscall/js" +) + +const ( + // databaseSuffix is the suffix to be appended to the name of + // the database. + databaseSuffix = "_speakeasy_dm" + + // currentVersion is the current version of the IndexDb + // runtime. Used for migration purposes. + currentVersion uint = 1 +) + +// 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, pubKey ed25519.PublicKey, update bool) + +// 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, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) (dm.Receiver, error) { + databaseName := path + databaseSuffix + return newWASMModel(databaseName, encryption, cb) +} + +// newWASMModel creates the given [idb.Database] and returns a wasmModel. +func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) (*wasmModel, error) { + // Attempt to open database object + ctx, cancel := indexedDb.NewContext() + defer cancel() + openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + if oldVersion == newVersion { + jww.INFO.Printf("IndexDb version is current: v%d", + newVersion) + return nil + } + + jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", + oldVersion, newVersion) + + if oldVersion == 0 && newVersion >= 1 { + err := v1Upgrade(db) + if err != nil { + return err + } + oldVersion = 1 + } + + // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } + return nil + }) + if err != nil { + return nil, err + } + + // Wait for database open to finish + db, err := openRequest.Await(ctx) + if err != nil { + return nil, err + } + + // Save the encryption status to storage + encryptionStatus := encryption != nil + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + databaseName, encryptionStatus) + if err != nil { + return nil, err + } + + // Verify encryption status does not change + if encryptionStatus != loadedEncryptionStatus { + return nil, errors.New( + "Cannot load database with different encryption status.") + } else if !encryptionStatus { + jww.WARN.Printf("IndexedDb encryption disabled!") + } + + // Attempt to ensure the database has been properly initialized + openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + return nil + }) + if err != nil { + return nil, err + } + // Wait for database open to finish + db, err = openRequest.Await(ctx) + if err != nil { + return nil, err + } + wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} + + return wrapper, nil +} + +// v1Upgrade performs the v0 -> v1 database upgrade. +// +// This can never be changed without permanently breaking backwards +// compatibility. +func v1Upgrade(db *idb.Database) error { + indexOpts := idb.IndexOptions{ + Unique: false, + MultiEntry: false, + } + + // Build Message ObjectStore and Indexes + messageStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(msgPkeyName), + AutoIncrement: true, + } + messageStore, err := db.CreateObjectStore(messageStoreName, messageStoreOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreMessageIndex, + js.ValueOf(messageStoreMessage), + idb.IndexOptions{ + Unique: true, + MultiEntry: false, + }) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreConversationIndex, + js.ValueOf(messageStoreConversation), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreParentIndex, + js.ValueOf(messageStoreParent), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreTimestampIndex, + js.ValueOf(messageStoreTimestamp), indexOpts) + if err != nil { + return err + } + + // Build Channel ObjectStore + conversationStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(convoPkeyName), + AutoIncrement: false, + } + _, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts) + if err != nil { + return err + } + + // Get the database name and save it to storage + if databaseName, err := db.Name(); err != nil { + return err + } else if err = storage.StoreIndexedDb(databaseName); err != nil { + return err + } + + return nil +} diff --git a/indexedDb/dm/model.go b/indexedDb/dm/model.go new file mode 100644 index 0000000000000000000000000000000000000000..bb6f34588aa2976c1689b629c6df31219de6b585 --- /dev/null +++ b/indexedDb/dm/model.go @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "time" +) + +const ( + // Text representation of primary key value (keyPath). + msgPkeyName = "id" + convoPkeyName = "pub_key" + + // Text representation of the names of the various [idb.ObjectStore]. + messageStoreName = "messages" + conversationStoreName = "conversations" + + // Message index names. + messageStoreMessageIndex = "message_id_index" + messageStoreConversationIndex = "conversation_id_index" + messageStoreParentIndex = "parent_message_id_index" + messageStoreTimestampIndex = "timestamp_index" + + // Message keyPath names (must match json struct tags). + messageStoreMessage = "message_id" + messageStoreConversation = "conversation_id" + messageStoreParent = "parent_message_id" + messageStoreTimestamp = "timestamp" +) + +// Message defines the IndexedDb representation of a single Message. +// +// A Message belongs to one Conversation. +// A Message may belong to one Message (Parent). +type Message struct { + ID uint64 `json:"id"` // Matches msgPkeyName + MessageID []byte `json:"message_id"` // Index + ConversationPubKey []byte `json:"conversation_pub_key"` // Index + ParentMessageID []byte `json:"parent_message_id"` // Index + Timestamp time.Time `json:"timestamp"` // Index + Status uint8 `json:"status"` + Text []byte `json:"text"` + Type uint16 `json:"type"` + Round uint64 `json:"round"` +} + +// Conversation defines the IndexedDb representation of a single +// message exchange between two recipients. +// A Conversation has many Message. +type Conversation struct { + Pubkey []byte `json:"pub_key"` // Matches convoPkeyName + Nickname string `json:"nickname"` + Token uint32 `json:"token"` + CodesetVersion uint8 `json:"codeset_version"` + Blocked bool `json:"blocked"` +} diff --git a/indexedDb/implementation_test.go b/indexedDb/implementation_test.go deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/indexedDb/utils.go b/indexedDb/utils.go index 1aa6ff45805c1c65b9ad27e51a8b0590e7b8201d..a8e7fd8e528a185cef11f65baa351bc602a9a745 100644 --- a/indexedDb/utils.go +++ b/indexedDb/utils.go @@ -22,9 +22,13 @@ import ( "time" ) -// dbTimeout is the global timeout for operations with the storage -// [context.Context]. -const dbTimeout = time.Second +const ( + // dbTimeout is the global timeout for operations with the storage + // [context.Context]. + dbTimeout = time.Second + // ErrDoesNotExist is an error string for got undefined on Get operations. + ErrDoesNotExist = "result is undefined" +) // NewContext builds a context for indexedDb operations. func NewContext() (context.Context, context.CancelFunc) { @@ -62,8 +66,8 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) } else if resultObj.IsUndefined() { - return js.Undefined(), errors.WithMessage(parentErr, - "Unable to get from ObjectStore: result is undefined") + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %s", ErrDoesNotExist) } // Process result into string @@ -111,8 +115,8 @@ func GetIndex(db *idb.Database, objectStoreName string, return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) } else if resultObj.IsUndefined() { - return js.Undefined(), errors.WithMessage(parentErr, - "Unable to get from ObjectStore: result is undefined") + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %s", ErrDoesNotExist) } // Process result into string diff --git a/wasm/channels.go b/wasm/channels.go index 447bea74ea42a6674c3859f10a12ef5ab2bf6fce..66dd9e656a156c275d692a7a7bfb7ca2efadaf7c 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -1284,7 +1284,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - nickname - The nickname of the sender of the message (string). // - text - The content of the message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1303,7 +1303,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - 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, pubKey []byte, dmToken []byte, codeset int, timestamp, lease, roundId, msgType, + text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, status int64) int64 { uuid := em.receiveMessage(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), nickname, text, @@ -1329,7 +1329,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, // - senderUsername - The username of the sender of the message (string). // - text - The content of the message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1348,7 +1348,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, // - 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, pubKey []byte, dmToken []byte, codeset int, timestamp, lease, + senderUsername, text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, status int64) int64 { uuid := em.receiveReply(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo), @@ -1374,7 +1374,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // - senderUsername - The username of the sender of the message (string). // - reaction - The contents of the reaction message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1393,7 +1393,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // - 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, pubKey []byte, dmToken []byte, codeset int, timestamp, + senderUsername, reaction string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, status int64) int64 { uuid := em.receiveReaction(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),