diff --git a/README.md b/README.md index a322234acd7f1470c369a0d10714dc80ddf45142..87ac6d17986684565ddae99f0406643f6f2b4545 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,19 @@ WebAssembly bindings for xxDK. $ GOOS=js GOARCH=wasm go build -o xxdk.wasm ``` +### Running Tests + +To run unit tests, you need to first install +[wasmbrowsertest](https://github.com/agnivade/wasmbrowsertest). + +`wasm/wasm_js.s` contains commands only recognized by the Go WebAssembly +compiler and thus cause compile errors when running tests. It needs to be +excluded when running tests. + +```shell +$ GOOS=js GOARCH=wasm go test +``` + ## Testing The `test` directory contains a website and server to run the compiled diff --git a/main.go b/main.go index a7edc16bac3ffa8f5ad22a88731672de45a80173..9e5d8932e68f54b251dc1955cc98168e3e18a7bc 100644 --- a/main.go +++ b/main.go @@ -89,8 +89,10 @@ func main() { js.FuncOf(wasm.UpdateCommonErrors)) // bindings/fileTransfer.go - js.Global().Set("InitFileTransfer", - js.FuncOf(wasm.InitFileTransfer)) + js.Global().Set("InitFileTransfer", js.FuncOf(wasm.InitFileTransfer)) + + // bindings/group.go + js.Global().Set("NewGroupChat", js.FuncOf(wasm.NewGroupChat)) <-make(chan bool) os.Exit(0) diff --git a/wasm/group.go b/wasm/group.go new file mode 100644 index 0000000000000000000000000000000000000000..d75c10a5624a4b9235930b9564be5d602e16f220 --- /dev/null +++ b/wasm/group.go @@ -0,0 +1,364 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package wasm + +import ( + "gitlab.com/elixxir/client/bindings" + "syscall/js" +) + +//////////////////////////////////////////////////////////////////////////////// +// Group Chat // +//////////////////////////////////////////////////////////////////////////////// + +// GroupChat wraps the [bindings.GroupChat] object so its methods can be wrapped +// to be Javascript compatible. +type GroupChat struct { + api *bindings.GroupChat +} + +// newGroupChatJS creates a new Javascript compatible object +// (map[string]interface{}) that matches the GroupChat structure. +func newGroupChatJS(api *bindings.GroupChat) map[string]interface{} { + gc := GroupChat{api} + gcMap := map[string]interface{}{ + "MakeGroup": js.FuncOf(gc.MakeGroup), + "ResendRequest": js.FuncOf(gc.ResendRequest), + "JoinGroup": js.FuncOf(gc.JoinGroup), + "Send": js.FuncOf(gc.Send), + "GetGroups": js.FuncOf(gc.GetGroups), + "GetGroup": js.FuncOf(gc.GetGroup), + "NumGroups": js.FuncOf(gc.NumGroups), + } + + return gcMap +} + +// NewGroupChat creates a bindings-layer group chat manager. +// +// Parameters: +// - args[0] - ID of E2e object in tracker (int). +// - args[1] - Javascript object that has functions that implement the +// [bindings.GroupRequest] interface. +// - args[2] - Javascript object that has functions that implement the +// [bindings.GroupChatProcessor] interface. +// +// Returns: +// - Javascript representation of the GroupChat object. +// - Throws a TypeError if creating the GroupChat fails. +func NewGroupChat(_ js.Value, args []js.Value) interface{} { + requestFunc := &groupRequest{args[1].Get("Callback").Invoke} + p := &groupChatProcessor{ + args[2].Get("Process").Invoke, args[2].Get("String").Invoke} + + api, err := bindings.NewGroupChat(args[0].Int(), requestFunc, p) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return newGroupChatJS(api) +} + +// MakeGroup creates a new Group and sends a group request to all members in the +// group. +// +// Parameters: +// - args[0] - JSON of array of [id.ID]; it contains the IDs of members the +// user wants to add to the group (Uint8Array). +// - args[1] - the initial message sent to all members in the group. This is an +// optional parameter and may be nil (Uint8Array). +// - args[2] - the name of the group decided by the creator. This is an +// optional parameter and may be nil. If nil the group will be assigned the +// default name (Uint8Array). +// +// Returns: +// - JSON of [bindings.GroupReport], which can be passed into +// Cmix.WaitForRoundResult to see if the group request message send +// succeeded. +// - Throws a TypeError if making the group fails. +func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) interface{} { + // (membershipBytes, message, name []byte) ([]byte, error) + membershipBytes := CopyBytesToGo(args[0]) + message := CopyBytesToGo(args[1]) + name := CopyBytesToGo(args[2]) + + report, err := g.api.MakeGroup(membershipBytes, message, name) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return CopyBytesToJS(report) +} + +// ResendRequest resends a group request to all members in the group. +// +// Parameters: +// - args[0] - group's ID (Uint8Array). This can be found in the report +// returned by GroupChat.MakeGroup. +// +// Returns: +// - JSON of [bindings.GroupReport] (Uint8Array), which can be passed into +// Cmix.WaitForRoundResult to see if the group request message send +// succeeded. +// - Throws a TypeError if resending the request fails. +func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) interface{} { + report, err := g.api.ResendRequest(CopyBytesToGo(args[0])) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return CopyBytesToJS(report) +} + +// JoinGroup allows a user to join a group when a request is received. +// If an error is returned, handle it properly first; you may then retry later +// with the same trackedGroupId. +// +// Parameters: +// - args[0] - ID of the Group object in tracker (int). This is received by +// [bindings.GroupRequest.Callback]. +// +// Returns: +// - Throws a TypeError if joining the group fails. +func (g *GroupChat) JoinGroup(_ js.Value, args []js.Value) interface{} { + err := g.api.JoinGroup(args[0].Int()) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return nil +} + +// LeaveGroup deletes a group so a user no longer has access. +// +// Parameters: +// - args[0] - group's ID (Uint8Array). This can be found in the report +// returned by GroupChat.MakeGroup. +// +// Returns: +// - Throws a TypeError if leaving the group fails. +func (g *GroupChat) LeaveGroup(_ js.Value, args []js.Value) interface{} { + err := g.api.LeaveGroup(CopyBytesToGo(args[0])) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return nil +} + +// Send is the bindings-level function for sending to a group. +// +// Parameters: +// - args[0] - group's ID (Uint8Array). This can be found in the report +// returned by GroupChat.MakeGroup. +// - args[1] - the message that the user wishes to send to the group +// (Uint8Array). +// - args[2] - the tag associated with the message (string). This tag may be +// empty. +// +// Returns: +// - JSON of [bindings.GroupSendReport] (Uint8Array), which can be passed into +// Cmix.WaitForRoundResult to see if the group message send succeeded. +func (g *GroupChat) Send(_ js.Value, args []js.Value) interface{} { + groupId := CopyBytesToGo(args[0]) + message := CopyBytesToGo(args[1]) + + report, err := g.api.Send(groupId, message, args[2].String()) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return CopyBytesToJS(report) +} + +// GetGroups returns a list of group IDs that the user is a member of. +// +// Returns: +// - JSON of array of [id.ID] representing all group ID's (Uint8Array). +// - Throws a TypeError if getting the groups fails. +func (g *GroupChat) GetGroups(js.Value, []js.Value) interface{} { + // () ([]byte, error) + groups, err := g.api.GetGroups() + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return CopyBytesToJS(groups) +} + +// GetGroup returns the group with the group ID. If no group exists, then the +// error "failed to find group" is returned. +// +// Parameters: +// - args[0] - group's ID (Uint8Array). This can be found in the report +// returned by GroupChat.MakeGroup. +// +// Returns: +// - Javascript representation of the Group object. +// - Throws a TypeError if getting the group fails +func (g *GroupChat) GetGroup(_ js.Value, args []js.Value) interface{} { + grp, err := g.api.GetGroup(CopyBytesToGo(args[0])) + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return newGroupJS(grp) +} + +// NumGroups returns the number of groups the user is a part of. +// +// Returns: +// - int +func (g *GroupChat) NumGroups(js.Value, []js.Value) interface{} { + return g.api.NumGroups() +} + +//////////////////////////////////////////////////////////////////////////////// +// Group Structure // +//////////////////////////////////////////////////////////////////////////////// + +// Group wraps the [bindings.Group] object so its methods can be wrapped to be +// Javascript compatible. +type Group struct { + api *bindings.Group +} + +// newGroupJS creates a new Javascript compatible object +// (map[string]interface{}) that matches the Group structure. +func newGroupJS(api *bindings.Group) map[string]interface{} { + g := Group{api} + gMap := map[string]interface{}{ + "GetName": js.FuncOf(g.GetName), + "GetID": js.FuncOf(g.GetID), + "GetTrackedID": js.FuncOf(g.GetTrackedID), + "GetInitMessage": js.FuncOf(g.GetInitMessage), + "GetCreatedNano": js.FuncOf(g.GetCreatedNano), + "GetCreatedMS": js.FuncOf(g.GetCreatedMS), + "GetMembership": js.FuncOf(g.GetMembership), + "Serialize": js.FuncOf(g.Serialize), + } + + return gMap +} + +// GetName returns the name set by the user for the group. +// +// Returns: +// - Uint8Array +func (g *Group) GetName(js.Value, []js.Value) interface{} { + return CopyBytesToJS(g.api.GetName()) +} + +// GetID return the 33-byte unique group ID. This represents the id.ID object. +// +// Returns: +// - Uint8Array +func (g *Group) GetID(js.Value, []js.Value) interface{} { + return CopyBytesToJS(g.api.GetID()) +} + +// GetTrackedID returns the tracked ID of the Group object. This is used by the +// backend tracker. +// +// Returns: +// - int +func (g *Group) GetTrackedID(js.Value, []js.Value) interface{} { + return g.api.GetTrackedID() +} + +// GetInitMessage returns initial message sent with the group request. +// +// Returns: +// - Uint8Array +func (g *Group) GetInitMessage(js.Value, []js.Value) interface{} { + return CopyBytesToJS(g.api.GetInitMessage()) +} + +// GetCreatedNano returns the time the group was created in nanoseconds. This is +// also the time the group requests were sent. +// +// Returns: +// - int +func (g *Group) GetCreatedNano(js.Value, []js.Value) interface{} { + return g.api.GetCreatedNano() +} + +// GetCreatedMS returns the time the group was created in milliseconds. This is +// also the time the group requests were sent. +// +// Returns: +// - int +func (g *Group) GetCreatedMS(js.Value, []js.Value) interface{} { + return g.api.GetCreatedMS() +} + +// GetMembership retrieves a list of group members. The list is in order; +// the first contact is the leader/creator of the group. +// All subsequent members are ordered by their ID. +// +// Returns: +// - JSON of [group.Membership] (Uint8Array). +// - Throws a TypeError if marshalling fails. +func (g *Group) GetMembership(js.Value, []js.Value) interface{} { + membership, err := g.api.GetMembership() + if err != nil { + Throw(TypeError, err.Error()) + return nil + } + + return CopyBytesToJS(membership) +} + +// Serialize serializes the Group. +// +// Returns: +// - Byte representation of the Group (Uint8Array). +func (g *Group) Serialize(js.Value, []js.Value) interface{} { + return CopyBytesToJS(g.api.Serialize()) +} + +//////////////////////////////////////////////////////////////////////////////// +// Callbacks // +//////////////////////////////////////////////////////////////////////////////// + +// groupRequest wraps Javascript callbacks to adhere to the +// [bindings.GroupRequest] interface. +type groupRequest struct { + callback func(args ...interface{}) js.Value +} + +func (gr *groupRequest) Callback(g *bindings.Group) { + gr.callback(newGroupJS(g)) +} + +// groupChatProcessor wraps Javascript callbacks to adhere to the +// [bindings.GroupChatProcessor] interface. +type groupChatProcessor struct { + callback func(args ...interface{}) js.Value + string func(args ...interface{}) js.Value +} + +func (gcp *groupChatProcessor) Process(decryptedMessage, msg, + receptionId []byte, ephemeralId, roundId int64, err error) { + gcp.callback(CopyBytesToJS(decryptedMessage), CopyBytesToJS(msg), + CopyBytesToJS(receptionId), ephemeralId, roundId, err.Error()) +} + +func (gcp *groupChatProcessor) String() string { + return gcp.string().String() +} diff --git a/wasm/group_test.go b/wasm/group_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fbd4b8879f77e335c950f43c2d574a1af5f50a63 --- /dev/null +++ b/wasm/group_test.go @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package wasm + +import ( + "gitlab.com/elixxir/client/bindings" + "reflect" + "testing" +) + +// Tests that the map representing GroupChat returned by newGroupChatJS contains +// all of the methods on GroupChat. +func Test_newGroupChatJS(t *testing.T) { + gcType := reflect.TypeOf(&GroupChat{}) + + gc := newGroupChatJS(&bindings.GroupChat{}) + if len(gc) != gcType.NumMethod() { + t.Errorf("GroupChat JS object does not have all methods."+ + "\nexpected: %d\nreceived: %d", gcType.NumMethod(), len(gc)) + } + + for i := 0; i < gcType.NumMethod(); i++ { + method := gcType.Method(i) + + if _, exists := gc[method.Name]; !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} + +// Tests that the map representing Group returned by newGroupJS contains all of +// the methods on Group. +func Test_newGroupJS(t *testing.T) { + gType := reflect.TypeOf(&Group{}) + + g := newGroupJS(&bindings.Group{}) + if len(g) != gType.NumMethod() { + t.Errorf("Group JS object does not have all methods."+ + "\nexpected: %d\nreceived: %d", gType.NumMethod(), len(g)) + } + + for i := 0; i < gType.NumMethod(); i++ { + method := gType.Method(i) + + if _, exists := g[method.Name]; !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +}