Skip to content
Snippets Groups Projects
channels.go 91.72 KiB
////////////////////////////////////////////////////////////////////////////////
// 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 wasm

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"sync"
	"syscall/js"

	"gitlab.com/elixxir/client/v4/bindings"
	"gitlab.com/elixxir/client/v4/channels"
	"gitlab.com/elixxir/wasm-utils/exception"
	"gitlab.com/elixxir/wasm-utils/utils"
	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
)

////////////////////////////////////////////////////////////////////////////////
// Basic Channel API                                                          //
////////////////////////////////////////////////////////////////////////////////

// ChannelsManager wraps the [bindings.ChannelsManager] object so its methods
// can be wrapped to be Javascript compatible.
type ChannelsManager struct {
	api *bindings.ChannelsManager
}

// newChannelsManagerJS creates a new Javascript compatible object
// (map[string]any) that matches the [ChannelsManager] structure.
func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
	cm := ChannelsManager{api}
	channelsManagerMap := map[string]any{
		// Basic Channel API
		"GetID":                 js.FuncOf(cm.GetID),
		"GenerateChannel":       js.FuncOf(cm.GenerateChannel),
		"JoinChannel":           js.FuncOf(cm.JoinChannel),
		"GetChannels":           js.FuncOf(cm.GetChannels),
		"LeaveChannel":          js.FuncOf(cm.LeaveChannel),
		"ReplayChannel":         js.FuncOf(cm.ReplayChannel),
		"EnableDirectMessages":  js.FuncOf(cm.EnableDirectMessages),
		"DisableDirectMessages": js.FuncOf(cm.DisableDirectMessages),
		"AreDMsEnabled":         js.FuncOf(cm.AreDMsEnabled),

		// Share URL
		"GetShareURL": js.FuncOf(cm.GetShareURL),

		// Channel Sending Methods and Reports
		"SendGeneric":           js.FuncOf(cm.SendGeneric),
		"SendAdminGeneric":      js.FuncOf(cm.SendAdminGeneric),
		"SendMessage":           js.FuncOf(cm.SendMessage),
		"SendReply":             js.FuncOf(cm.SendReply),
		"SendReaction":          js.FuncOf(cm.SendReaction),
		"SendSilent":            js.FuncOf(cm.SendSilent),
		"SendInvite":            js.FuncOf(cm.SendInvite),
		"DeleteMessage":         js.FuncOf(cm.DeleteMessage),
		"PinMessage":            js.FuncOf(cm.PinMessage),
		"MuteUser":              js.FuncOf(cm.MuteUser),
		"GetIdentity":           js.FuncOf(cm.GetIdentity),
		"ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity),
		"GetStorageTag":         js.FuncOf(cm.GetStorageTag),
		"SetNickname":           js.FuncOf(cm.SetNickname),
		"DeleteNickname":        js.FuncOf(cm.DeleteNickname),
		"GetNickname":           js.FuncOf(cm.GetNickname),
		"Muted":                 js.FuncOf(cm.Muted),
		"GetMutedUsers":         js.FuncOf(cm.GetMutedUsers),
		"IsChannelAdmin":        js.FuncOf(cm.IsChannelAdmin),
		"ExportChannelAdminKey": js.FuncOf(cm.ExportChannelAdminKey),
		"VerifyChannelAdminKey": js.FuncOf(cm.VerifyChannelAdminKey),
		"ImportChannelAdminKey": js.FuncOf(cm.ImportChannelAdminKey),
		"DeleteChannelAdminKey": js.FuncOf(cm.DeleteChannelAdminKey),

		// Channel Receiving Logic and Callback Registration
		"RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler),

		// Notifications
		"GetNotificationLevel":  js.FuncOf(cm.GetNotificationLevel),
		"GetNotificationStatus": js.FuncOf(cm.GetNotificationStatus),
		"SetMobileNotificationsLevel": js.FuncOf(
			cm.SetMobileNotificationsLevel),
	}

	return channelsManagerMap
}

// GetID returns the ID for this [ChannelsManager] in the [ChannelsManager]
// tracker.
//
// Returns:
//   - Tracker ID (int).
func (cm *ChannelsManager) GetID(js.Value, []js.Value) any {
	return cm.api.GetID()
}

// GenerateChannelIdentity creates a new private channel identity
// ([channel.PrivateIdentity]) from scratch and assigns it a codename.
//
// The public component can be retrieved as JSON via
// [GetPublicChannelIdentityFromPrivate].
//
// Parameters:
//   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
//     using [Cmix.GetID].
//
// Returns:
//   - Marshalled bytes of [channel.PrivateIdentity] (Uint8Array).
//   - Throws an error if generating the identity fails.
func GenerateChannelIdentity(_ js.Value, args []js.Value) any {
	pi, err := bindings.GenerateChannelIdentity(args[0].Int())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(pi)
}

// identityMap stores identities previously generated by ConstructIdentity.
var identityMap sync.Map

// ConstructIdentity creates a codename in a public [channel.Identity] from an
// extant identity for a given codeset version.
//
// Parameters:
//   - args[0] - The Ed25519 public key (Uint8Array).
//   - args[1] - The version of the codeset used to generate the identity (int).
//
// Returns:
//   - JSON of [channel.Identity] (Uint8Array).
//   - Throws an error if constructing the identity fails.
func ConstructIdentity(_ js.Value, args []js.Value) any {
	// Note: This function is similar to constructIdentity below except that it
	//  uses a sync.Map backend to increase efficiency for identities that were
	//  already generated in this browser session.

	pubKey := utils.CopyBytesToGo(args[0])
	pubKeyBase64 := base64.StdEncoding.EncodeToString(pubKey)
	identityObj, exists := identityMap.Load(pubKeyBase64)
	if exists {
		return utils.CopyBytesToJS(identityObj.([]byte))
	}

	identity, err := bindings.ConstructIdentity(
		utils.CopyBytesToGo(args[0]), args[1].Int())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	identityMap.Store(base64.StdEncoding.EncodeToString(pubKey), identity)

	return utils.CopyBytesToJS(identity)
}

// constructIdentity constructs a [channel.Identity] from a user's public key
// and codeset version. This function is retain for benchmarking purposes.
//
// Parameters:
//   - args[0] - The Ed25519 public key (Uint8Array).
//   - args[1] - The version of the codeset used to generate the identity (int).
//
// Returns:
//   - JSON of [channel.Identity] (Uint8Array).
//   - Throws an error if constructing the identity fails.
func constructIdentity(_ js.Value, args []js.Value) any {
	identity, err := bindings.ConstructIdentity(
		utils.CopyBytesToGo(args[0]), args[1].Int())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(identity)
}

// ImportPrivateIdentity generates a new [channel.PrivateIdentity] from exported
// data.
//
// Parameters:
//   - args[0] - The password used to encrypt the identity (string).
//   - args[2] - The encrypted data from [ChannelsManager.ExportPrivateIdentity]
//     (Uint8Array).
//
// Returns:
//   - JSON of [channel.PrivateIdentity] (Uint8Array).
//   - Throws an error if importing the identity fails.
func ImportPrivateIdentity(_ js.Value, args []js.Value) any {
	password := args[0].String()
	data := utils.CopyBytesToGo(args[1])

	pi, err := bindings.ImportPrivateIdentity(password, data)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	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 an error if unmarshalling the bytes or marshalling the identity
//     fails.
func GetPublicChannelIdentity(_ js.Value, args []js.Value) any {
	marshaledPublic := utils.CopyBytesToGo(args[0])
	pi, err := bindings.GetPublicChannelIdentity(marshaledPublic)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	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 an error if unmarshalling the bytes or marshalling the identity
//     fails.
func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
	marshaledPrivate := utils.CopyBytesToGo(args[0])
	identity, err :=
		bindings.GetPublicChannelIdentityFromPrivate(marshaledPrivate)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(identity)
}

// 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 [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].
//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
//     IDs. The ID can be retrieved from an object with an extension builder
//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
//     using extension builders. Example: `[2,11,5]` (Uint8Array).
//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
//     using [Notifications.GetID] (int).
//   - args[5] - A Javascript object that implements the function on
//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
//     various events. The entire interface can be nil, but if defined, each
//     method must be implemented.
//
// Returns:
//   - Javascript representation of the [ChannelsManager] object.
//   - Throws an error if creating the manager fails.
func NewChannelsManager(_ js.Value, args []js.Value) any {
	cmixId := args[0].Int()
	privateIdentity := utils.CopyBytesToGo(args[1])
	em := newEventModelBuilder(args[2])
	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
	notificationsID := args[4].Int()
	cUI := newChannelUI(args[5])

	cm, err := bindings.NewChannelsManager(
		cmixId, privateIdentity, em, extensionBuilderIDsJSON, notificationsID, cUI)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return newChannelsManagerJS(cm)
}

// LoadChannelsManager loads an existing [ChannelsManager] for the given storage
// tag.
//
// 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 [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 initializes and returns a Javascript object
//     that matches the [bindings.EventModel] interface. The function must match
//     the Build function in [bindings.EventModelBuilder].
//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
//     IDs. The ID can be retrieved from an object with an extension builder
//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
//     using extension builders. Example: `[2,11,5]`.
//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
//     using [Notifications.GetID] (int).
//   - args[5] - A Javascript object that implements the function on
//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
//     various events. The entire interface can be nil, but if defined, each
//     method must be implemented.
//
// Returns:
//   - Javascript representation of the [ChannelsManager] object.
//   - Throws an error if loading the manager fails.
func LoadChannelsManager(_ js.Value, args []js.Value) any {
	cmixID := args[0].Int()
	storageTag := args[1].String()
	em := newEventModelBuilder(args[2])
	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
	notificationsID := args[4].Int()
	cUI := newChannelUI(args[5])
	cm, err := bindings.LoadChannelsManager(
		cmixID, storageTag, em, extensionBuilderIDsJSON, notificationsID, cUI)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return newChannelsManagerJS(cm)
}

// 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] - Path to Javascript file that starts the worker (string).
//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
//     generated by [GenerateChannelIdentity] (Uint8Array).
//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
//     IDs. The ID can be retrieved from an object with an extension builder
//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
//     using extension builders. Example: `[2,11,5]` (Uint8Array).
//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
//     using [Notifications.GetID] (int).
//   - args[5] - A Javascript object that implements the function on
//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
//     various events. The entire interface can be nil, but if defined, each
//     method must be implemented.
//   - args[6] - ID of [DbCipher] object in tracker (int). Create this
//     object with [NewDatabaseCipher] and get its id with
//     [DbCipher.GetID].
//
// Returns a promise:
//   - Resolves to a Javascript representation of the [ChannelsManager] object.
//   - Rejected with an error if loading indexedDb or the manager fails.
//   - Throws an error if the cipher ID does not correspond to a cipher.
func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
	cmixID := args[0].Int()
	wasmJsPath := args[1].String()
	privateIdentity := utils.CopyBytesToGo(args[2])
	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
	notificationsID := args[4].Int()
	cUI := newChannelUI(args[5])
	cipherID := args[6].Int()

	cipher, err := dbCipherTrackerSingleton.get(cipherID)
	if err != nil {
		exception.ThrowTrace(err)
	}

	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
		extensionBuilderIDsJSON, notificationsID, cUI, cipher)
}

// NewChannelsManagerWithIndexedDbUnsafe creates a new [ChannelsManager] from a
// new private identity ([channel.PrivateIdentity]) and using indexedDb as a
// backend to manage the event model. However, the data is written in plain text
// and not encrypted. It is recommended that you do not use this in production.
//
// 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 [LoadChannelsManagerWithIndexedDbUnsafe],
// 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] - Path to Javascript file that starts the worker (string).
//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
//     generated by [GenerateChannelIdentity] (Uint8Array).
//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
//     IDs. The ID can be retrieved from an object with an extension builder
//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
//     using extension builders. Example: `[2,11,5]` (Uint8Array).
//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
//     using [Notifications.GetID] (int).
//   - args[5] - A Javascript object that implements the function on
//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
//     various events. The entire interface can be nil, but if defined, each
//     method must be implemented.
//
// Returns a promise:
//   - Resolves to a Javascript representation of the [ChannelsManager] object.
//   - Rejected with an error if loading indexedDb or the manager fails.
//
// FIXME: package names in comments for indexedDb
func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
	cmixID := args[0].Int()
	wasmJsPath := args[1].String()
	privateIdentity := utils.CopyBytesToGo(args[2])
	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
	notificationsID := args[4].Int()
	cUI := newChannelUI(args[5])

	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
		extensionBuilderIDsJSON, notificationsID, cUI, nil)
}

func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
	privateIdentity, extensionBuilderIDsJSON []byte, notificationsID int,
	channelsCbs bindings.ChannelUICallbacks, cipher *DbCipher) any {

	model := channelsDb.NewWASMEventModelBuilder(
		wasmJsPath, cipher.api, channelsCbs)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		cm, err := bindings.NewChannelsManagerGoEventModel(cmixID,
			privateIdentity, extensionBuilderIDsJSON, model, notificationsID,
			channelsCbs)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(newChannelsManagerJS(cm))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// 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
//     using [Cmix.GetID].
//   - args[1] - Path to Javascript file that starts the worker (string).
//   - args[2] - The storage tag associated with the previously created channel
//     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
//     IDs. The ID can be retrieved from an object with an extension builder
//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
//     using extension builders. Example: `[2,11,5]` (Uint8Array).
//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
//     using [Notifications.GetID] (int).
//   - args[5] - A Javascript object that implements the function on
//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
//     various events. The entire interface can be nil, but if defined, each
//     method must be implemented.
//   - args[6] - ID of [DbCipher] object in tracker (int). Create this
//     object with [NewDatabaseCipher] and get its id with
//     [DbCipher.GetID].
//
// Returns a promise:
//   - Resolves to a Javascript representation of the [ChannelsManager] object.
//   - Rejected with an error if loading indexedDb or the manager fails.
//   - Throws an error if the cipher ID does not correspond to a cipher.
func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
	cmixID := args[0].Int()
	wasmJsPath := args[1].String()
	storageTag := args[2].String()
	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
	notificationsID := args[4].Int()
	channelsCbs := newChannelUI(args[5])
	cipherID := args[6].Int()

	cipher, err := dbCipherTrackerSingleton.get(cipherID)
	if err != nil {
		exception.ThrowTrace(err)
	}

	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
		extensionBuilderIDsJSON, notificationsID, channelsCbs, cipher)
}

// LoadChannelsManagerWithIndexedDbUnsafe loads an existing [ChannelsManager]
// using an existing indexedDb database as a backend to manage the event model.
// This should only be used to load unsafe channel managers created by
// [NewChannelsManagerWithIndexedDbUnsafe].
//
// 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
//     using [Cmix.GetID].
//   - args[1] - Path to Javascript file that starts the worker (string).
//   - args[2] - The storage tag associated with the previously created channel
//     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
//     IDs. The ID can be retrieved from an object with an extension builder
//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
//     using extension builders. Example: `[2,11,5]` (Uint8Array).
//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
//     using [Notifications.GetID] (int).
//   - args[5] - A Javascript object that implements the function on
//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
//     various events. The entire interface can be nil, but if defined, each
//     method must be implemented.
//
// Returns a promise:
//   - Resolves to a Javascript representation of the [ChannelsManager] object.
//   - Rejected with an error if loading indexedDb or the manager fails.
func LoadChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
	cmixID := args[0].Int()
	wasmJsPath := args[1].String()
	storageTag := args[2].String()
	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
	notificationsID := args[4].Int()
	cUI := newChannelUI(args[5])

	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
		extensionBuilderIDsJSON, notificationsID, cUI, nil)
}

func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
	extensionBuilderIDsJSON []byte, notificationsID int,
	channelsCbs bindings.ChannelUICallbacks, cipher *DbCipher) any {

	model := channelsDb.NewWASMEventModelBuilder(
		wasmJsPath, cipher.api, channelsCbs)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		cm, err := bindings.LoadChannelsManagerGoEventModel(
			cmixID, storageTag, model, extensionBuilderIDsJSON, notificationsID,
			channelsCbs)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(newChannelsManagerJS(cm))
		}
	}

	return utils.CreatePromise(promiseFn)
}

////////////////////////////////////////////////////////////////////////////////
// Channel Actions                                                            //
////////////////////////////////////////////////////////////////////////////////

// DecodePublicURL decodes the channel URL into a channel pretty print. This
// function can only be used for public channel URLs. To get the privacy level
// of a channel URL, use [GetShareUrlType].
//
// Parameters:
//   - args[0] - The channel's share URL (string). Should be received from
//     another user or generated via [ChannelsManager.GetShareURL].
//
// Returns:
//   - The channel pretty print (string).
func DecodePublicURL(_ js.Value, args []js.Value) any {
	c, err := bindings.DecodePublicURL(args[0].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return c
}

// DecodePrivateURL decodes the channel URL, using the password, into a channel
// pretty print. This function can only be used for private or secret channel
// URLs. To get the privacy level of a channel URL, use [GetShareUrlType].
//
// Parameters:
//   - args[0] - The channel's share URL (string). Should be received from
//     another user or generated via [ChannelsManager.GetShareURL].
//   - args[1] - The password needed to decrypt the secret data in the URL
//     (string).
//
// Returns:
//   - The channel pretty print (string)
func DecodePrivateURL(_ js.Value, args []js.Value) any {
	c, err := bindings.DecodePrivateURL(args[0].String(), args[1].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return c
}

// DecodeInviteURL decodes the channel URL, using the password, into a channel
// pretty print. This function can only be used for URLs from invitations.
//
// Parameters:
//   - args[0] - The channel's share URL (string). Should be received from
//     another user via invitation.
//   - args[1] - The password needed to decrypt the secret data in the URL
//     (string).
//
// Returns:
//   - The channel pretty print (string)
func DecodeInviteURL(_ js.Value, args []js.Value) any {
	var (
		url      = args[0].String()
		password = args[1].String()
	)

	c, err := bindings.DecodeInviteURL(url, password)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return c
}

// GetChannelJSON returns the JSON of the channel for the given pretty print.
//
// Parameters:
//   - args[0] - The pretty print of the channel (string).
//
// Returns:
//   - JSON of the [broadcast.Channel] object (Uint8Array).
//
// Example JSON of [broadcast.Channel]:
//
//	{
//	  "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
//	  "Name": "My_Channel",
//	  "Description": "Here is information about my channel.",
//	  "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
//	  "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
//	  "RsaPubKeyLength": 5,
//	  "RSASubPayloads": 1,
//	  "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
//	  "Level": 0
//	}
func GetChannelJSON(_ js.Value, args []js.Value) any {
	c, err := bindings.GetChannelJSON(args[0].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(c)
}

// GetChannelInfo returns the info about a channel from its public description.
//
// Parameters:
//   - args[0] - The pretty print of the channel (string).
//
// The pretty print will be of the format:
//
//	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
//
// Returns:
//   - JSON of [bindings.ChannelInfo], which describes all relevant channel info
//     (Uint8Array).
//   - Throws an error if getting the channel info fails.
func GetChannelInfo(_ js.Value, args []js.Value) any {
	ci, err := bindings.GetChannelInfo(args[0].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(ci)
}

// GenerateChannel creates a new channel with the user as the admin and returns
// the pretty print of the channel. This function only create a channel and does
// not join it.
//
// The private key is saved to storage and can be accessed with
// [ChannelsManager.ExportChannelAdminKey].
//
// Parameters:
//   - args[0] - The name of the new channel (string). The name must be between
//     3 and 24 characters inclusive. It can only include upper and lowercase
//     Unicode letters, digits 0 through 9, and underscores (_). It cannot be
//     changed once a channel is created.
//   - args[1] - The description of a channel (string). The description is
//     optional but cannot be longer than 144 characters and can include all
//     Unicode characters. It cannot be changed once a channel is created.
//   - args[2] - The [broadcast.PrivacyLevel] of the channel (int). 0 = public,
//     1 = private, and 2 = secret. Refer to the comment below for more
//     information.
//
// Returns a promise:
//   - Resolves to the pretty print of the channel (string).
//   - Rejected with an error if generating the channel fails.
//
// The [broadcast.PrivacyLevel] of a channel indicates the level of channel
// information revealed when sharing it via URL. For any channel besides public
// channels, the secret information is encrypted and a password is required to
// share and join a channel.
//   - A privacy level of [broadcast.Public] reveals all the information
//     including the name, description, privacy level, public key and salt.
//   - A privacy level of [broadcast.Private] reveals only the name and
//     description.
//   - A privacy level of [broadcast.Secret] reveals nothing.
func (cm *ChannelsManager) GenerateChannel(_ js.Value, args []js.Value) any {
	name := args[0].String()
	description := args[1].String()
	privacyLevel := args[2].Int()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		prettyPrint, err :=
			cm.api.GenerateChannel(name, description, privacyLevel)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(prettyPrint)
		}
	}

	return utils.CreatePromise(promiseFn)
}

// JoinChannel joins the given channel. It will return the error
// [channels.ChannelAlreadyExistsErr] if the channel has already been joined.
//
// Parameters:
//   - args[0] - A portable channel string. Should be received from another user
//     or generated via [ChannelsManager.GenerateChannel] (string).
//
// The pretty print will be of the format:
//
//	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelInfo], which describes all
//     relevant channel information (Uint8Array).
//   - Rejected with an error if joining the channel fails.
func (cm *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any {
	channelPretty := args[0].String()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		ci, err := cm.api.JoinChannel(channelPretty)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(ci))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// LeaveChannel leaves the given channel. It will return the error
// [channels.ChannelDoesNotExistsErr] if the channel was not previously joined.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//
// Returns a promise:
//   - Resolves on success (void).
//   - Rejected with an error if the channel does not exist.
func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := cm.api.LeaveChannel(marshalledChanId)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// ReplayChannel replays all messages from the channel within the network's
// memory (~3 weeks) over the event model.
//
// Returns the error [channels.ChannelDoesNotExistsErr] if the channel was not
// previously joined.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
//
// Returns:
//   - Throws an error if the replay fails.
func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])

	err := cm.api.ReplayChannel(marshalledChanId)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return nil
}

// GetChannels returns the IDs of all channels that have been joined.
//
// Returns:
//   - JSON of an array of marshalled [id.ID] (Uint8Array).
//   - Throws an error if getting the channels fails.
//
// JSON Example:
//
//	{
//	  "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
//	  "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
//	}
func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any {
	channelList, err := cm.api.GetChannels()
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(channelList)
}

// EnableDirectMessages enables the token for direct messaging for this
// channel.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//
// Returns:
//   - Throws an error if saving the DM token fails.
func (cm *ChannelsManager) EnableDirectMessages(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	err := cm.api.EnableDirectMessages(marshalledChanId)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}
	return nil
}

// DisableDirectMessages removes the token for direct messaging for a
// given channel.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//
// Returns:
//   - Throws an error if saving the DM token fails
func (cm *ChannelsManager) DisableDirectMessages(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	err := cm.api.DisableDirectMessages(marshalledChanId)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}
	return nil
}

// AreDMsEnabled returns the status of direct messaging for a given channel.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//
// Returns:
//   - enabled (bool) - status of dms for passed in channel ID, true if enabled
//   - Throws an error if unmarshalling the channel ID
func (cm *ChannelsManager) AreDMsEnabled(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	enabled, err := cm.api.AreDMsEnabled(marshalledChanId)
	if err != nil {
		exception.ThrowTrace(err)
		return false
	}
	return enabled
}

////////////////////////////////////////////////////////////////////////////////
// Channel Share URL                                                          //
////////////////////////////////////////////////////////////////////////////////

// ShareURL is returned by GetShareURL containing the sharable URL and a
// password, if the channel is private.
type ShareURL struct {
	URL      string `json:"url"`
	Password string `json:"password"`
}

// GetShareURL generates a URL that can be used to share this channel with
// others on the given host.
//
// A URL comes in one of three forms based on the privacy level set when
// generating the channel. Each privacy level hides more information than the
// last with the lowest level revealing everything and the highest level
// revealing nothing. For any level above the lowest, a password is returned,
// which will be required when decoding the URL.
//
// The maxUses is the maximum number of times this URL can be used to join a
// channel. If it is set to 0, then it can be shared unlimited times. The max
// uses is set as a URL parameter using the key [broadcast.MaxUsesKey]. Note
// that this number is also encoded in the secret data for private and secret
// URLs, so if the number is changed in the URL, it will be verified when
// calling [DecodePublicURL] and [DecodePrivateURL]. There is no enforcement for
// public URLs.
//
// Parameters:
//   - args[0] - ID of [Cmix] object in tracker (int).
//   - args[1] - The URL to append the channel info to (string).
//   - args[2] - The maximum number of uses the link can be used (0 for
//     unlimited) (int).
//   - args[3] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//
// Returns:
//   - JSON of [bindings.ShareURL] (Uint8Array).
//   - Throws an error if generating the URL fails.
func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
	cmixID := args[0].Int()
	host := args[1].String()
	maxUses := args[2].Int()
	marshalledChanId := utils.CopyBytesToGo(args[3])

	su, err := cm.api.GetShareURL(cmixID, host, maxUses, marshalledChanId)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(su)
}

// GetShareUrlType determines the [broadcast.PrivacyLevel] of the channel URL.
// If the URL is an invalid channel URL, an error is returned.
//
// Parameters:
//   - args[0] - The channel share URL (string).
//
// Returns:
//   - An int that corresponds to the [broadcast.PrivacyLevel] as outlined
//     below (int).
//   - Throws an error if parsing the URL fails.
//
// Possible returns:
//
//	0 = public channel
//	1 = private channel
//	2 = secret channel
func GetShareUrlType(_ js.Value, args []js.Value) any {
	level, err := bindings.GetShareUrlType(args[0].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return level
}

////////////////////////////////////////////////////////////////////////////////
// Channel Sending Methods and Reports                                        //
////////////////////////////////////////////////////////////////////////////////

// ValidForever returns the value to use for validUntil when you want a message
// to be available for the maximum amount of time.
//
// Returns:
//   - The maximum amount of time (int).
func ValidForever(js.Value, []js.Value) any {
	return bindings.ValidForever()
}

// SendGeneric is used to send a raw message over a channel. In general, it
// should be wrapped in a function that defines the wire protocol.
//
// If the final message, before being sent over the wire, is too long, this
// will return an error. Due to the underlying encoding using compression,
// it is not possible to define the largest payload that can be sent, but it
// will always be possible to send a payload of 802 bytes at minimum.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - args[1] - The message type of the message. This will be a valid
//     [channels.MessageType] (int).
//   - args[2] - The contents of the message (Uint8Array).
//   - args[3] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[4] - Set tracked to true if the message should be tracked in the
//     sendTracker, which allows messages to be shown locally before they are
//     received on the network. In general, all messages that will be displayed
//     to the user should be tracked while all actions should not be (boolean).
//   - args[5] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//   - args[6] - JSON of a map of slices of [ed25519.PublicKey] of users that
//     should receive mobile notifications for the message. Each slice keys on a
//     [channels.PingType] that describes the type of notification it is
//     (Uint8Array).
//
// Example map of slices of public keys:
//
//	{
//	  "usrMention": [
//	    "CLdKxbe8D2WVOpx1mT63TZ5CP/nesmxHLT5DUUalpe0=",
//	    "S2c6NXjNqgR11SCOaiQUughWaLpWBKNufPt6cbTVHMA="
//	  ],
//	  "usrReply": [
//	    "aaMzSeA6Cu2Aix2MlOwzrAI+NnpKshzvZRT02PZPVec="
//	  ]
//	}
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	messageType := args[1].Int()
	msg := utils.CopyBytesToGo(args[2])
	leaseTimeMS := int64(args[3].Int())
	tracked := args[4].Bool()
	cmixParamsJSON := utils.CopyBytesToGo(args[5])
	pingsMapJSON := utils.CopyBytesToGo(args[6])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType,
			msg, leaseTimeMS, tracked, cmixParamsJSON, pingsMapJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// SendMessage 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 798 bytes at minimum.
//
// 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).
//   - args[1] - The contents of the message (string).
//   - args[2] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[3] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//   - args[4] - JSON of a slice of public keys of users that should receive
//     mobile notifications for the message.
//
// Example slice of public keys:
//
//	[
//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
//	]
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	msg := args[1].String()
	leaseTimeMS := int64(args[2].Int())
	cmixParamsJSON := utils.CopyBytesToGo(args[3])
	pingsJSON := utils.CopyBytesToGo(args[4])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendMessage(
			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, pingsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// SendReply is used to send a formatted message over a channel.
//
// Due to the underlying encoding using compression, it is not 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 that the reply is sent to does not exist, then the other
// side will post the message as a normal message and not as a reply.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - args[1] - The contents of the message. The message should be at most 510
//     bytes. This is expected to be Unicode, and thus a string data type is
//     expected (string).
//   - args[2] - JSON of [channel.MessageID] of the message you wish to reply
//     to. This may be found in the [bindings.ChannelSendReport] if replying to
//     your own. Alternatively, if reacting to another user's message, you may
//     retrieve it via the [bindings.ChannelMessageReceptionCallback] registered
//     using  RegisterReceiveHandler (Uint8Array).
//   - args[3] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//   - args[5] - JSON of a slice of public keys of users that should receive
//     mobile notifications for the message.
//
// Example slice of public keys:
//
//	[
//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
//	]
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	msg := args[1].String()
	messageToReactTo := utils.CopyBytesToGo(args[2])
	leaseTimeMS := int64(args[3].Int())
	cmixParamsJSON := utils.CopyBytesToGo(args[4])
	pingsJSON := utils.CopyBytesToGo(args[5])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendReply(marshalledChanId, msg,
			messageToReactTo, leaseTimeMS, cmixParamsJSON, pingsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// SendReaction is used to send a reaction to a message over a channel. The
// reaction must be a single emoji with no other characters, and will be
// rejected otherwise.
//
// Clients will drop the reaction if they do not recognize the reactTo message.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - args[1] - The user's reaction. This should be a single emoji with no
//     other characters. As such, a Unicode string is expected (string).
//   - args[2] - JSON of [channel.MessageID] of the message you wish to reply
//     to. This may be found in the [bindings.ChannelSendReport] if replying to
//     your own. Alternatively, if reacting to another user's message, you may
//     retrieve it via the ChannelMessageReceptionCallback registered using
//     RegisterReceiveHandler (Uint8Array).
//   - args[3] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
	var (
		marshalledChanId = utils.CopyBytesToGo(args[0])
		reaction         = args[1].String()
		messageToReactTo = utils.CopyBytesToGo(args[2])
		leaseTimeMS      = int64(args[3].Int())
		cmixParamsJSON   = utils.CopyBytesToGo(args[4])
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendReaction(marshalledChanId, reaction,
			messageToReactTo, leaseTimeMS, cmixParamsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// SendSilent is used to send to a channel a message with no notifications.
// Its primary purpose is to communicate new nicknames without calling
// [SendMessage].
//
// It takes no payload intentionally as the message should be very lightweight.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - args[1] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[2] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendSilent(_ js.Value, args []js.Value) any {
	var (
		marshalledChanId = utils.CopyBytesToGo(args[0])
		leaseTimeMS      = int64(args[1].Int())
		cmixParamsJSON   = utils.CopyBytesToGo(args[2])
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendSilent(
			marshalledChanId, leaseTimeMS, cmixParamsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// SendInvite is used to send to a channel (invited) an invitation to another
// channel (invitee).
//
// If the channel ID for the invitee channel is not recognized by the Manager,
// then an error will be returned.
//
// Parameters:
//   - args[0] - Marshalled bytes of the invited channel [id.ID] (Uint8Array).
//   - args[1] - JSON of the invitee channel [id.ID].
//     This can be retrieved from [GetChannelJSON]. (Uint8Array).
//   - args[2] - The contents of the message (string).
//   - args[3] - The URL to append the channel info to (string).
//   - args[4] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[5] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//   - args[6] - JSON of a slice of public keys of users that should receive
//     mobile notifications for the message.
//
// Example slice of public keys:
//
//	[
//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
//	]
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendInvite(_ js.Value, args []js.Value) any {
	var (
		marshalledChanId = utils.CopyBytesToGo(args[0])
		inviteToJSON     = utils.CopyBytesToGo(args[1])
		msg              = args[2].String()
		host             = args[3].String()
		leaseTimeMS      = int64(args[4].Int())
		cmixParamsJSON   = utils.CopyBytesToGo(args[5])
		pingsJSON        = utils.CopyBytesToGo(args[6])
	)

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendInvite(marshalledChanId,
			inviteToJSON, msg, host, leaseTimeMS,
			cmixParamsJSON, pingsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

////////////////////////////////////////////////////////////////////////////////
// Admin Sending                                                              //
////////////////////////////////////////////////////////////////////////////////

// SendAdminGeneric is used to send a raw message over a channel encrypted with
// admin keys, identifying it as sent by the admin. In general, it should be
// wrapped in a function that defines the wire protocol.
//
// If the final message, before being sent over the wire, is too long, this will
// return an error. The message must be at most 510 bytes long.
//
// If the user is not an admin of the channel (i.e. does not have a private key
// for the channel saved to storage), then the error [channels.NotAnAdminErr] is
// returned.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - args[1] - The message type of the message. This will be a valid
//     [channels.MessageType] (int).
//   - args[2] - The contents of the message (Uint8Array). The message should be
//     at most 510 bytes.
//   - args[3] - The lease of the message. This will be how long the
//     message is available from the network, in milliseconds (int). As per the
//     [channels.Manager] documentation, this has different meanings depending
//     on the use case. These use cases may be generic enough that they will not
//     be enumerated here. Use [ValidForever] to last the max message life.
//   - args[4] - Set tracked to true if the message should be tracked in the
//     sendTracker, which allows messages to be shown locally before they are
//     received on the network. In general, all messages that will be displayed
//     to the user should be tracked while all actions should not be (boolean).
//   - args[5] - JSON of [xxdk.CMIXParams]. If left empty
//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
	marshalledChanId := utils.CopyBytesToGo(args[0])
	messageType := args[1].Int()
	msg := utils.CopyBytesToGo(args[2])
	leaseTimeMS := int64(args[3].Int())
	tracked := args[4].Bool()
	cmixParamsJSON := utils.CopyBytesToGo(args[5])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.SendAdminGeneric(marshalledChanId,
			messageType, msg, leaseTimeMS, tracked, cmixParamsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// DeleteMessage deletes the targeted message from user's view. Users may delete
// their own messages but only the channel admin can delete other user's
// messages. If the user is not an admin of the channel or if they are not the
// sender of the targetMessage, then the error [channels.NotAnAdminErr] is
// returned.
//
// If undoAction is true, then the targeted message is un-deleted.
//
// Clients will drop the deletion if they do not recognize the target
// message.
//
// Parameters:
//   - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array).
//   - args[1] - The marshalled [channel.MessageID] of the message you want to
//     delete (Uint8Array).
//   - args[2] - JSON of [xxdk.CMIXParams]. This may be empty, and
//     [GetDefaultCMixParams] will be used internally (Uint8Array).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) DeleteMessage(_ js.Value, args []js.Value) any {
	channelIdBytes := utils.CopyBytesToGo(args[0])
	targetMessageIdBytes := utils.CopyBytesToGo(args[1])
	cmixParamsJSON := utils.CopyBytesToGo(args[2])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.DeleteMessage(
			channelIdBytes, targetMessageIdBytes, cmixParamsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// PinMessage pins the target message to the top of a channel view for all users
// in the specified channel. Only the channel admin can pin user messages; if
// the user is not an admin of the channel, then the error
// [channels.NotAnAdminErr] is returned.
//
// If undoAction is true, then the targeted message is unpinned.
//
// Clients will drop the pin if they do not recognize the target message.
//
// Parameters:
//   - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array).
//   - args[1] - The marshalled [channel.MessageID] of the message you want to
//     pin (Uint8Array).
//   - args[2] - Set to true to unpin the message (boolean).
//   - args[3] - The time, in milliseconds, that the message should be pinned
//     (int). To remain pinned indefinitely, use [ValidForever].
//   - args[4] - JSON of [xxdk.CMIXParams]. This may be empty, and
//     [GetDefaultCMixParams] will be used internally (Uint8Array).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) PinMessage(_ js.Value, args []js.Value) any {
	channelIdBytes := utils.CopyBytesToGo(args[0])
	targetMessageIdBytes := utils.CopyBytesToGo(args[1])
	undoAction := args[2].Bool()
	validUntilMS := args[3].Int()
	cmixParamsJSON := utils.CopyBytesToGo(args[4])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.PinMessage(channelIdBytes,
			targetMessageIdBytes, undoAction, validUntilMS, cmixParamsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

// MuteUser is used to mute a user in a channel. Muting a user will cause all
// future messages from the user being dropped on reception. Muted users are
// also unable to send messages. Only the channel admin can mute a user; if the
// user is not an admin of the channel, then the error [channels.NotAnAdminErr]
// is returned.
//
// If undoAction is true, then the targeted user will be unmuted.
//
// Parameters:
//   - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array).
//   - args[1] - The [ed25519.PublicKey] of the user you want to mute
//     (Uint8Array).
//   - args[2] - Set to true to unmute the message (boolean).
//   - args[3] - The time, in milliseconds, that the user should be muted (int).
//     To remain muted indefinitely, use [ValidForever].
//   - args[4] - JSON of [xxdk.CMIXParams]. This may be empty, and
//     [GetDefaultCMixParams] will be used internally (Uint8Array).
//
// Returns a promise:
//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
//   - Rejected with an error if sending fails.
func (cm *ChannelsManager) MuteUser(_ js.Value, args []js.Value) any {
	channelIdBytes := utils.CopyBytesToGo(args[0])
	mutedUserPubKeyBytes := utils.CopyBytesToGo(args[1])
	undoAction := args[2].Bool()
	validUntilMS := args[3].Int()
	cmixParamsJSON := utils.CopyBytesToGo(args[4])

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		sendReport, err := cm.api.MuteUser(channelIdBytes, mutedUserPubKeyBytes,
			undoAction, validUntilMS, cmixParamsJSON)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve(utils.CopyBytesToJS(sendReport))
		}
	}

	return utils.CreatePromise(promiseFn)
}

////////////////////////////////////////////////////////////////////////////////
// Other Channel Actions                                                      //
////////////////////////////////////////////////////////////////////////////////

// GetIdentity returns the public identity ([channel.Identity]) of the user
// associated with this channel manager.
//
// Returns:
//   - JSON of the [channel.Identity] (Uint8Array).
//   - Throws TypeError if marshalling the identity fails.
func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any {
	i, err := cm.api.GetIdentity()
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(i)
}

// ExportPrivateIdentity encrypts the private identity using the password and
// exports it to a portable string.
//
// Parameters:
//   - password - The password used to encrypt the private identity (string).
//
// Returns:
//   - Encrypted portable private identity (Uint8Array).
//   - Throws TypeError if exporting the identity fails.
func (cm *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
	i, err := cm.api.ExportPrivateIdentity(args[0].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}
	return utils.CopyBytesToJS(i)
}

// GetStorageTag returns the tag where this manager is stored. To be used when
// loading the manager. The storage tag is derived from the public key.
//
// Returns:
//   - Storage tag (string).
func (cm *ChannelsManager) GetStorageTag(js.Value, []js.Value) any {
	return cm.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 (cm *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any {
	err := cm.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1]))
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return nil
}

// DeleteNickname removes the nickname for a given channel. The name will revert
// back to the codename for this channel instead.
//
// Parameters:
//   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
//
// Returns:
//   - Throws TypeError if deleting the nickname fails.
func (cm *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any {
	err := cm.api.DeleteNickname(utils.CopyBytesToGo(args[0]))
	if err != nil {
		exception.ThrowTrace(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 set for the channel (string).
//   - Throws TypeError if the channel has no nickname set.
func (cm *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any {
	nickname, err := cm.api.GetNickname(utils.CopyBytesToGo(args[0]))
	if err != nil {
		exception.ThrowTrace(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) any {
	err := bindings.IsNicknameValid(args[0].String())
	if err != nil {
		return exception.NewError(err)
	}

	return nil
}

// Muted returns true if the user is currently muted in the given channel.
//
// Parameters:
//   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
//
// Returns:
//   - Returns true if the user is muted in the channel and false otherwise
//     (boolean).
//   - Throws an error if the channel ID cannot be unmarshalled.
func (cm *ChannelsManager) Muted(_ js.Value, args []js.Value) any {
	channelIDBytes := utils.CopyBytesToGo(args[0])

	muted, err := cm.api.Muted(channelIDBytes)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return muted
}

// GetMutedUsers returns the list of the public keys for each muted user in
// the channel. If there are no muted user or if the channel does not exist,
// an empty list is returned.
//
// Parameters:
//   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
//
// Returns:
//   - JSON of an array of ed25519.PublicKey (Uint8Array). Look below for an
//     example.
//   - Throws an error if the channel ID cannot be unmarshalled.
//
// Example return:
//
//	["k2IrybDXjJtqxjS6Tx/6m3bXvT/4zFYOJnACNWTvESE=","ocELv7KyeCskLz4cm0klLWhmFLYvQL2FMDco79GTXYw=","mmxoDgoTEYwaRyEzq5Npa24IIs+3B5LXhll/8K5yCv0="]
func (cm *ChannelsManager) GetMutedUsers(_ js.Value, args []js.Value) any {
	channelIDBytes := utils.CopyBytesToGo(args[0])
	mutedUsers, err := cm.api.GetMutedUsers(channelIDBytes)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(mutedUsers)
}

////////////////////////////////////////////////////////////////////////////////
// Notifications                                                              //
////////////////////////////////////////////////////////////////////////////////

// GetNotificationLevel returns the [channels.NotificationLevel] for the given
// channel.
//
// Parameters:
//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array).
//
// Returns:
//   - The [channels.NotificationLevel] for the channel (int).
//   - Throws an error if the channel ID cannot be unmarshalled or the channel
//     cannot be found.
func (cm *ChannelsManager) GetNotificationLevel(_ js.Value, args []js.Value) any {
	channelIDBytes := utils.CopyBytesToGo(args[0])

	level, err := cm.api.GetNotificationLevel(channelIDBytes)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return level
}

// GetNotificationStatus returns the notification status for the given channel.
//
// Parameters:
//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array).
//
// Returns:
//   - The [notifications.NotificationState] for the channel (int).
//   - Throws an error if the channel ID cannot be unmarshalled or the channel
//     cannot be found.
func (cm *ChannelsManager) GetNotificationStatus(_ js.Value, args []js.Value) any {
	channelIDBytes := utils.CopyBytesToGo(args[0])

	status, err := cm.api.GetNotificationStatus(channelIDBytes)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return status
}

// SetMobileNotificationsLevel sets the notification level for the given
// channel. The [channels.NotificationLevel] dictates the type of notifications
// received and the status controls weather the notification is push or in-app.
// If muted, both the level and status must be set to mute.
//
// To use push notifications, a token must be registered with the notification
// manager. Note, when enabling push notifications, information may be shared
// with third parties (i.e., Firebase and Google's Palantir) and may represent a
// security risk to the user.
//
// Parameters:
//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array).
//   - args[1] - The [channels.NotificationLevel] to set for the channel (int).
//   - args[2] - The [notifications.NotificationState] to set for the channel
//     (int).
//
// Returns a promise and throws an error if setting the notification
// level fails.
func (cm *ChannelsManager) SetMobileNotificationsLevel(_ js.Value,
	args []js.Value) any {
	channelIDBytes := utils.CopyBytesToGo(args[0])
	level := args[1].Int()
	status := args[2].Int()

	promiseFn := func(resolve, reject func(args ...any) js.Value) {
		err := cm.api.SetMobileNotificationsLevel(channelIDBytes,
			level, status)
		if err != nil {
			reject(exception.NewTrace(err))
		} else {
			resolve()
		}
	}

	return utils.CreatePromise(promiseFn)
}

// GetChannelNotificationReportsForMe checks the notification data against the
// filter list to determine which notifications belong to the user. A list of
// notification reports is returned detailing all notifications for the user.
//
// Parameters:
//   - notificationFilterJSON - JSON of a slice of [channels.NotificationFilter].
//     It can optionally be the entire json return from
//     [bindings.NotificationUpdateJson] instead of just the needed subsection
//     (Uint8Array).
//   - notificationDataCSV - CSV containing notification data (string).
//
// Example JSON of a slice of [channels.NotificationFilter]:
//
//	 [
//	   {
//		    "identifier": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUDYXN5bUlkZW50aWZpZXI=",
//		    "channelID": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUD",
//		    "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"],
//		    "allowLists": {
//		      "allowWithTags": {},
//		      "allowWithoutTags": {"102":{}, "2":{}}
//		    }
//		  },
//		  {
//		    "identifier": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUDc3ltSWRlbnRpZmllcg==",
//		    "channelID": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUD",
//		    "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"],
//		    "allowLists": {
//		      "allowWithTags": {},
//		      "allowWithoutTags": {"1":{}, "40000":{}}
//		    }
//		  },
//		  {
//		    "identifier": "jCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlQDYXN5bUlkZW50aWZpZXI=",
//		    "channelID": "jCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlQD",
//		    "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"],
//		    "allowLists": {
//		      "allowWithTags": {},
//		      "allowWithoutTags": {"102":{}, "2":{}}
//		    }
//		  }
//		]
//
// Returns:
//   - The JSON of a slice of [channels.NotificationReport] (Uint8Array).
//   - Throws an error if getting the report fails.
//
// Example return:
//
//	[
//	  {
//	    "channel": "jOgZopfYj4zrE/AHtKmkf+QEWnfUKv9KfIy/+Bsg0PkD",
//	    "type": 1,
//	    "pingType": "usrMention"
//	  },
//	  {
//	    "channel": "GKmfN/LKXQYM6++TC6DeZYqoxvSUPkh5UAHWODqh9zkD",
//	    "type": 2,
//	    "pingType": "usrReply"
//	  },
//	  {
//	    "channel": "M+28xtj0coHrhDHfojGNcyb2c4maO7ZuheB6egS0Pc4D",
//	    "type": 1,
//	    "pingType": ""
//	  }
//	]
func GetChannelNotificationReportsForMe(_ js.Value, args []js.Value) any {
	notificationFilterJSON := utils.CopyBytesToGo(args[0])
	notificationDataCSV := args[1].String()

	report, err := bindings.GetChannelNotificationReportsForMe(
		notificationFilterJSON, notificationDataCSV)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return utils.CopyBytesToJS(report)
}

////////////////////////////////////////////////////////////////////////////////
// Admin Management                                                           //
////////////////////////////////////////////////////////////////////////////////

// IsChannelAdmin returns true if the user is an admin of the channel.
//
// Parameters:
//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array)
//
// Returns:
//   - True if the user is an admin in the channel and false otherwise
//     (boolean).
//   - Throws an error if the channel ID cannot be unmarshalled.
func (cm *ChannelsManager) IsChannelAdmin(_ js.Value, args []js.Value) any {
	isAdmin, err := cm.api.IsChannelAdmin(utils.CopyBytesToGo(args[0]))
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return isAdmin
}

// ExportChannelAdminKey gets the private key for the given channel ID, encrypts
// it with the provided encryptionPassword, and exports it into a portable
// format. Returns an error if the user is not an admin of the channel.
//
// This key can be provided to other users in a channel to grant them admin
// access using [ChannelsManager.ImportChannelAdminKey].
//
// The private key is encrypted using a key generated from the password using
// Argon2. Each call to ExportChannelAdminKey produces a different encrypted
// packet regardless if the same password is used for the same channel. It
// cannot be determined which channel the payload is for nor that two payloads
// are for the same channel.
//
// The passwords between each call are not related. They can be the same or
// different with no adverse impact on the security properties.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
//   - args[1] - The password used to encrypt the private key (string). The
//     passwords between each call are not related. They can be the same or
//     different with no adverse impact on the security properties.
//
// Returns:
//   - Portable string of the channel private key encrypted with the password
//     (Uint8Array).
//   - Throws an error if the user is not an admin for the channel.
func (cm *ChannelsManager) ExportChannelAdminKey(_ js.Value, args []js.Value) any {
	pk, err := cm.api.ExportChannelAdminKey(
		utils.CopyBytesToGo(args[0]), args[1].String())
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}
	return utils.CopyBytesToJS(pk)
}

// VerifyChannelAdminKey verifies that the encrypted private key can be
// decrypted and that it matches the expected channel. Returns false if private
// key does not belong to the given channel.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
//   - args[1] - The password used to encrypt the private key (string)
//   - args[2] - The encrypted channel private key packet (Uint8Array).
//
// Returns:
//   - Returns false if private key does not belong to the given channel ID
//     (boolean).
//   - Throws an error if the password is invalid.
//
// Returns:
//   - bool - True if the private key belongs to the channel and false
//     otherwise.
//   - Throws an error with the message [channels.WrongPasswordErr] for an
//     invalid password.
//   - Throws an error with the message [channels.ChannelDoesNotExistsErr] i
//     the channel has not already been joined.
func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) any {
	channelID := utils.CopyBytesToGo(args[0])
	encryptionPassword := args[1].String()
	encryptedPrivKey := utils.CopyBytesToGo(args[2])
	valid, err := cm.api.VerifyChannelAdminKey(
		channelID, encryptionPassword, encryptedPrivKey)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return valid
}

// ImportChannelAdminKey decrypts and imports the given encrypted private key
// and grants the user admin access to the channel the private key belongs to.
// Returns an error if the private key cannot be decrypted or if the private key
// is for the wrong channel.
//
// Parameters:
//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
//   - args[1] - The password used to encrypt the private key (string)
//   - args[2] - The encrypted channel private key packet (Uint8Array).
//
// Returns:
//   - Throws an error if the password is invalid or the private key does
//     not match the channel ID.
//   - Throws an error with the message [channels.WrongPasswordErr] for an
//     invalid password.
//   - Throws an error with the message [channels.ChannelDoesNotExistsErr] if
//     the channel has not already been joined.
//   - Throws an error with the message [channels.WrongPrivateKeyErr] if the
//     private key does not belong to the channel.
func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) any {
	channelID := utils.CopyBytesToGo(args[0])
	encryptionPassword := args[1].String()
	encryptedPrivKey := utils.CopyBytesToGo(args[2])
	err := cm.api.ImportChannelAdminKey(
		channelID, encryptionPassword, encryptedPrivKey)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return nil
}

// DeleteChannelAdminKey deletes the private key for the given channel.
//
// CAUTION: This will remove admin access. This cannot be undone. If the
// private key is deleted, it cannot be recovered and the channel can never
// have another admin.
//
// Parameters:
//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array)
//
// Returns:
//   - Throws an error if the deletion fails.
func (cm *ChannelsManager) DeleteChannelAdminKey(_ js.Value, args []js.Value) any {
	err := cm.api.DeleteChannelAdminKey(utils.CopyBytesToGo(args[0]))
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Channel Receiving Logic and Callback Registration                          //
////////////////////////////////////////////////////////////////////////////////

// channelMessageReceptionCallback wraps Javascript callbacks to adhere to the
// [bindings.ChannelMessageReceptionCallback] interface.
type channelMessageReceptionCallback struct {
	callback func(args ...any) js.Value
}

// Callback returns the context for a channel message.
//
// Parameters:
//   - 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) int {
	uuid := cmrCB.callback(
		utils.CopyBytesToJS(receivedChannelMessageReport), exception.NewTrace(err))

	return uuid.Int()
}

// RegisterReceiveHandler is used to register handlers for non-default message
// types. They can be processed by modules. It is important that such modules
// sync up with the event model implementation.
//
// There can only be one handler per [channels.MessageType], and this will
// return an error on any re-registration.
//
// Parameters:
//   - args[0] - The message type of the message. This will be a valid
//     [channels.MessageType] (int).
//   - args[1] - Javascript object that has functions that implement the
//     [bindings.ChannelMessageReceptionCallback] interface. This callback will
//     be executed when a channel message of the messageType is received.
//   - args[2] - A name describing what type of messages the listener picks up.
//     This is used for debugging and logging (string).
//   - args[3] - Set to true if this listener can receive messages from normal
//     users (boolean).
//   - args[4] - Set to true if this listener can receive messages from admins
//     (boolean).
//   - args[5] - Set to true if this listener can receive messages from muted
//     users (boolean).
//
// Returns:
//   - Throws an error if registering the handler fails.
func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any {
	messageType := args[0].Int()
	listenerCb := &channelMessageReceptionCallback{
		utils.WrapCB(args[1], "Callback")}
	name := args[2].String()
	userSpace := args[3].Bool()
	adminSpace := args[4].Bool()
	mutedSpace := args[5].Bool()

	err := cm.api.RegisterReceiveHandler(
		messageType, listenerCb, name, userSpace, adminSpace, mutedSpace)
	if err != nil {
		exception.ThrowTrace(err)
		return nil
	}

	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Event Model Logic                                                          //
////////////////////////////////////////////////////////////////////////////////

// GetNoMessageErr returns the error channels.NoMessageErr, which must be
// returned by EventModel methods (such as EventModel.UpdateFromUUID,
// EventModel.UpdateFromMessageID, and EventModel.GetMessage) when the message
// cannot be found.
//
// Returns:
//   - channels.NoMessageErr error message (string).
func GetNoMessageErr(js.Value, []js.Value) any {
	return bindings.GetNoMessageErr()
}

// CheckNoMessageErr determines if the error returned by an EventModel function
// indicates that the message or item does not exist. It returns true if the
// error contains channels.NoMessageErr.
//
// Parameters:
//   - args[0] - Error to check (Error).
//
// Returns
//   - True if the error contains channels.NoMessageErr (boolean).
func CheckNoMessageErr(_ js.Value, args []js.Value) any {
	return bindings.CheckNoMessageErr(js.Error{Value: args[0]}.Error())
}

// eventModelBuilder adheres to the [bindings.EventModelBuilder] interface.
type eventModelBuilder struct {
	build func(args ...any) js.Value
}

// newEventModelBuilder maps the methods on the Javascript object to a new
// eventModelBuilder.
func newEventModelBuilder(arg js.Value) *eventModelBuilder {
	return &eventModelBuilder{build: arg.Invoke}
}

// 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"),
		updateFromUUID:      utils.WrapCB(emJs, "UpdateFromUUID"),
		updateFromMessageID: utils.WrapCB(emJs, "UpdateFromMessageID"),
		getMessage:          utils.WrapCB(emJs, "GetMessage"),
		deleteMessage:       utils.WrapCB(emJs, "DeleteMessage"),
		muteUser:            utils.WrapCB(emJs, "MuteUser"),
	}
}

// eventModel wraps Javascript callbacks to adhere to the [bindings.EventModel]
// interface.
type eventModel struct {
	joinChannel         func(args ...any) js.Value
	leaveChannel        func(args ...any) js.Value
	receiveMessage      func(args ...any) js.Value
	receiveReply        func(args ...any) js.Value
	receiveReaction     func(args ...any) js.Value
	updateFromUUID      func(args ...any) js.Value
	updateFromMessageID func(args ...any) js.Value
	getMessage          func(args ...any) js.Value
	deleteMessage       func(args ...any) js.Value
	muteUser            func(args ...any) js.Value
}

// JoinChannel is called whenever a channel is joined locally.
//
// Parameters:
//   - channel - Returns the pretty print representation of a channel (string).
func (em *eventModel) JoinChannel(channel string) {
	em.joinChannel(channel)
}

// LeaveChannel is called whenever a channel is left locally.
//
// Parameters:
//   - ChannelId - Marshalled bytes of the channel [id.ID] (Uint8Array).
func (em *eventModel) LeaveChannel(channelID []byte) {
	em.leaveChannel(utils.CopyBytesToJS(channelID))
}

// ReceiveMessage is called whenever a message is received on a given channel.
// 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.
//
// Parameters:
//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - messageID - The bytes of the [channel.MessageID] of the received message
//     (Uint8Array).
//   - 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 (int32).
//   - codeset - The codeset version (int).
//   - 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) ReceiveMessage(channelID, messageID []byte, nickname,
	text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType,
	status int64, hidden bool) int64 {
	uuid := em.receiveMessage(utils.CopyBytesToJS(channelID),
		utils.CopyBytesToJS(messageID), nickname, text,
		utils.CopyBytesToJS(pubKey), dmToken, codeset, timestamp, lease, roundId,
		msgType, status, hidden)

	return int64(uuid.Int())
}

// ReceiveReply is called whenever a message is received that is a reply on a
// given channel. 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.
//
// Messages may arrive our of order, so a reply in theory can arrive before the
// initial message. As a result, it may be important to buffer replies.
//
// Parameters:
//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - messageID - The bytes of the [channel.MessageID] of the received message
//     (Uint8Array).
//   - reactionTo - The [channel.MessageID] for the message that received a
//     reply (Uint8Array).
//   - 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 (int32).
//   - codeset - The codeset version (int).
//   - 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, pubKey []byte, dmToken int32, codeset int, timestamp, lease,
	roundId, msgType, status int64, hidden bool) int64 {
	uuid := em.receiveReply(utils.CopyBytesToJS(channelID),
		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),
		senderUsername, text, utils.CopyBytesToJS(pubKey), dmToken, codeset,
		timestamp, lease, roundId, msgType, status, hidden)

	return int64(uuid.Int())
}

// ReceiveReaction is called whenever a reaction to a message is received on a
// given channel. It may be called multiple times on the same reaction. It is
// incumbent on the user of the API to filter such called by message ID.
//
// Messages may arrive our of order, so a reply in theory can arrive before the
// initial message. As a result, it may be important to buffer reactions.
//
// Parameters:
//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
//   - messageID - The bytes of the [channel.MessageID] of the received message
//     (Uint8Array).
//   - reactionTo - The [channel.MessageID] for the message that received a
//     reply (Uint8Array).
//   - 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 (int32).
//   - codeset - The codeset version (int).
//   - 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, pubKey []byte, dmToken int32, codeset int, timestamp,
	lease, roundId, msgType, status int64, hidden bool) int64 {
	uuid := em.receiveReaction(utils.CopyBytesToJS(channelID),
		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),
		senderUsername, reaction, utils.CopyBytesToJS(pubKey), dmToken, codeset,
		timestamp, lease, roundId, msgType, status, hidden)

	return int64(uuid.Int())
}

// UpdateFromUUID is called whenever a message at the UUID is modified.
//
// Parameters:
//   - uuid - The unique identifier of the message in the database (int).
//   - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo]
//     (Uint8Array).
//
// Returns:
//   - Returns an error if the message cannot be updated. It must return the
//     error from [GetNoMessageErr] if the message does not exist.
func (em *eventModel) UpdateFromUUID(
	uuid int64, messageUpdateInfoJSON []byte) error {
	err := em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON))
	return js.Error{Value: err}
}

// UpdateFromMessageID is called whenever a message with the message ID is
// modified.
//
// Note for developers: The internal Javascript function must return JSON of
// [UuidAndError], which includes the returned UUID or an error.
//
// Parameters:
//   - messageID - The bytes of the [channel.MessageID] of the received message
//     (Uint8Array).
//   - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo
//     (Uint8Array).
//
// Returns:
//   - A non-negative unique uuid for the modified message by which it can be
//     referenced later with [EventModel.UpdateFromUUID] int).
//   - Returns an error if the message cannot be updated. It must return the
//     error from [GetNoMessageErr] if the message does not exist.
func (em *eventModel) UpdateFromMessageID(
	messageID []byte, messageUpdateInfoJSON []byte) (int64, error) {
	uuidAndErrorBytes := utils.CopyBytesToGo(em.updateFromMessageID(
		utils.CopyBytesToJS(messageID),
		utils.CopyBytesToJS(messageUpdateInfoJSON)))

	var uae UuidAndError
	err := json.Unmarshal(uuidAndErrorBytes, &uae)
	if err != nil {
		return 0, err
	}

	if uae.Error != "" {
		return 0, errors.New(uae.Error)
	}

	return uae.UUID, nil
}

// GetMessage returns the message with the given [channel.MessageID].
//
// Note for developers: The internal Javascript function must return JSON of
// [MessageAndError], which includes the returned [channels.ModelMessage] or any
// error that occurs during lookup.
//
// Parameters:
//   - messageID - The bytes of the [channel.MessageID] of the message
//     (Uint8Array).
//
// Returns:
//   - JSON of [channels.ModelMessage] (Uint8Array).
func (em *eventModel) GetMessage(messageID []byte) ([]byte, error) {
	messageAndErrorBytes :=
		utils.CopyBytesToGo(em.getMessage(utils.CopyBytesToJS(messageID)))

	var mae MessageAndError
	err := json.Unmarshal(messageAndErrorBytes, &mae)
	if err != nil {
		return nil, err
	}

	if mae.Error != "" {
		return nil, errors.New(mae.Error)
	}

	return json.Marshal(mae.ModelMessage)
}

// DeleteMessage deletes the message with the given [channel.MessageID] from
// the database.
//
// Parameters:
//   - messageID - The bytes of the [channel.MessageID] of the message.
func (em *eventModel) DeleteMessage(messageID []byte) error {
	err := em.deleteMessage(utils.CopyBytesToJS(messageID))
	if !err.IsUndefined() {
		return js.Error{Value: err}
	}

	return nil
}

// MuteUser mutes the given user or unmutes them.
//
// Parameters:
//   - channelID - The bytes of the [id.ID] of the channel the user is being
//     muted in.
//   - pubKey - The [ed25519.PublicKey] of the user that is muted or unmuted.
func (em *eventModel) MuteUser(channelID, pubkey []byte, unmute bool) {
	em.muteUser(
		utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(pubkey), unmute)
}

// UuidAndError contains a UUID returned by an eventModel method or any possible
// error that occurs. Only one field should be present at a time.
//
// Example JSON:
//
//	{ "uuid": 5, }
//
// Or:
//
//	{ "error": "An error occurred." }
type UuidAndError struct {
	UUID  int64  `json:"uuid,omitempty"`
	Error string `json:"error,omitempty"`
}

// MessageAndError contains a message returned by eventModel.GetMessage or any
// possible error that occurs during lookup. Only one field should be present at
// a time; if an error occurs, ModelMessage should be empty.
//
// Example JSON:
//
//	{
//	  "ModelMessage": {
//	    "UUID": 50,
//	    "Nickname": "Nickname",
//	    "MessageID": "ODg5goFIBvpvqPzuoYGqmvxFYBgj0MMiQxAB51Q2nPs=",
//	    "ChannelID": "R+xKJTH6m4YRS4f0JggK3fTu10sANmtahS0Qtc8yi/AD",
//	    "ParentMessageID": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
//	    "Timestamp": "1955-11-05T12:01:00-07:00",
//	    "Lease": 21600000000000,
//	    "Status": 2,
//	    "Hidden": false,
//	    "Pinned": false,
//	    "Content": "VGhpcyBpcyBzb21lIG1lc3NhZ2UgY29udGVudC4=",
//	    "Type": 1,
//	    "Round": 7,
//	    "PubKey": "QyTtpndOf3sDZehVpOBQzQNBe1R2Eae7qlAEDZJ2mLg=",
//	    "CodesetVersion": 0
//	  },
//	  "Error": ""
//	}
type MessageAndError struct {
	// MessageJSON should contain the JSON of channels.ModelMessage.
	ModelMessage channels.ModelMessage

	// Error should only be filled when an error occurs on message lookup.
	Error string
}

// newChannelUI maps the methods on the Javascript object to the
// channelUI callbacks implementation struct.
func newChannelUI(cbImpl js.Value) *channelUI {
	return &channelUI{
		eventUpdate: utils.WrapCB(cbImpl, "EventUpdate"),
	}
}

// eventModel wraps Javascript callbacks to adhere to the
// [bindings.ChannelUICallbacks] interface.
type channelUI struct {
	eventUpdate func(args ...any) js.Value
}

// EventUpdate implements
// [bindings.ChannelUICallbacks.EventUpdate].
func (c *channelUI) EventUpdate(eventType int64, jsonData []byte) {
	c.eventUpdate(int(eventType), utils.CopyBytesToJS(jsonData))
}