diff --git a/bindings/channels.go b/bindings/channels.go new file mode 100644 index 0000000000000000000000000000000000000000..5ec0e502af3e0857e6cc5a029719186972f877d6 --- /dev/null +++ b/bindings/channels.go @@ -0,0 +1,701 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "encoding/json" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/channels" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" + "sync" + "time" +) + +//////////////////////////////////////////////////////////////////////////////// +// Singleton Tracker // +//////////////////////////////////////////////////////////////////////////////// + +// channelManagerTrackerSingleton is used to track ChannelsManager objects +// so that they can be referenced by ID back over the bindings. +var channelManagerTrackerSingleton = &channelManagerTracker{ + tracked: make(map[int]*ChannelsManager), + count: 0, +} + +// channelManagerTracker is a singleton used to keep track of extant +// ChannelsManager objects, preventing race conditions created by passing it +// over the bindings. +type channelManagerTracker struct { + tracked map[int]*ChannelsManager + count int + mux sync.RWMutex +} + +// make create a ChannelsManager from an [channels.Manager], assigns it a +// unique ID, and adds it to the channelManagerTracker. +func (cmt *channelManagerTracker) make(c channels.Manager) *ChannelsManager { + cmt.mux.Lock() + defer cmt.mux.Unlock() + + id := cmt.count + cmt.count++ + + cmt.tracked[id] = &ChannelsManager{ + api: c, + id: id, + } + + return cmt.tracked[id] +} + +// get an ChannelsManager from the channelManagerTracker given its ID. +func (cmt *channelManagerTracker) get(id int) (*ChannelsManager, error) { + cmt.mux.RLock() + defer cmt.mux.RUnlock() + + c, exist := cmt.tracked[id] + if !exist { + return nil, errors.Errorf( + "Cannot get ChannelsManager for ID %d, does not exist", id) + } + + return c, nil +} + +// delete removes a ChannelsManager from the channelManagerTracker. +func (cmt *channelManagerTracker) delete(id int) { + cmt.mux.Lock() + defer cmt.mux.Unlock() + + delete(cmt.tracked, id) +} + +//////////////////////////////////////////////////////////////////////////////// +// Basic Channel API // +//////////////////////////////////////////////////////////////////////////////// + +// ChannelsManager is a bindings-layer struct that wraps an [channels.Manager] +// interface. +type ChannelsManager struct { + api channels.Manager + id int +} + +// GetID returns the channelManagerTracker ID for the ChannelsManager object. +func (cm *ChannelsManager) GetID() int { + return cm.id +} + +// todo: docstring +func NewChannelsManager(e2eID, udID int) (*ChannelsManager, error) { + // Get user from singleton + user, err := e2eTrackerSingleton.get(e2eID) + if err != nil { + return nil, err + } + + udMan, err := udTrackerSingleton.get(udID) + if err != nil { + return nil, err + } + + nameService, err := udMan.api.StartChannelNameService() + if err != nil { + return nil, err + } + + // fixme: there is nothing adhering to event model + m := channels.NewManager(user.api.GetStorage().GetKV(), user.api.GetCmix(), + user.api.GetRng(), nameService, nil) + + // Add channel to singleton and return + return channelManagerTrackerSingleton.make(m), nil +} + +// JoinChannel joins the given channel. It will fail if the channel has already +// been joined. +// +// Parameters: +// - channelJson - A JSON encoded [ChannelDef]. This may be retrieved from +// [Channel.Get], for example.. +func (cm *ChannelsManager) JoinChannel(channelJson []byte) error { + // Unmarshal channel definition + def := ChannelDef{} + err := json.Unmarshal(channelJson, &def) + if err != nil { + return err + } + + // Construct ID using the embedded cryptographic information + channelId, err := cryptoBroadcast.NewChannelID(def.Name, def.Description, + def.Salt, def.PubKey) + if err != nil { + return err + } + + // Construct public key into object + rsaPubKey, err := rsa.LoadPublicKeyFromPem(def.PubKey) + if err != nil { + return err + } + + // Construct cryptographic channel object + channel := &cryptoBroadcast.Channel{ + ReceptionID: channelId, + Name: def.Name, + Description: def.Description, + Salt: def.Salt, + RsaPubKey: rsaPubKey, + } + + // Join the channel using the API + return cm.api.JoinChannel(channel) +} + +// GetChannels returns the IDs of all channels that have been joined. +// +// Returns: +// - []byte - A JSON marshalled list of IDs. +// JSON Example: +// { +// "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID", +// "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD" +// } +func (cm *ChannelsManager) GetChannels() ([]byte, error) { + channelIds := cm.api.GetChannels() + return json.Marshal(channelIds) +} + +// GetChannelId returns the ID of the channel given the channel's cryptographic +// information. +// +// Parameters: +// - channelJson - A JSON encoded [ChannelDef]. This may be retrieved from +// [Channel.Get], for example. +// +// Returns: +// - []byte - A JSON encoded channel ID ([id.ID]). +// JSON Example: +// "dGVzdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD" +func (cm *ChannelsManager) GetChannelId(channelJson []byte) ([]byte, error) { + def := ChannelDef{} + err := json.Unmarshal(channelJson, &def) + if err != nil { + return nil, err + } + + channelId, err := cryptoBroadcast.NewChannelID(def.Name, def.Description, + def.Salt, def.PubKey) + if err != nil { + return nil, err + } + + return json.Marshal(channelId) +} + +// GetChannel returns the underlying cryptographic structure for a given +// channel. +// +// Parameters: +// - []byte - A JSON marshalled channel ID ([id.ID]). This may be retrieved +// using ChannelsManager.GetChannelId. +// Returns: +// - []byte - A JSON marshalled ChannelDef. +func (cm *ChannelsManager) GetChannel(marshalledChanId []byte) ([]byte, error) { + // Unmarshal ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // Retrieve channel from manager + def, err := cm.api.GetChannel(chanId) + if err != nil { + return nil, err + } + + // Marshal channel + return json.Marshal(&ChannelDef{ + Name: def.Name, + Description: def.Description, + Salt: def.Salt, + PubKey: rsa.CreatePublicKeyPem(def.RsaPubKey), + }) +} + +// LeaveChannel leaves the given channel. It will return an error if the +// channel was not previously joined. +// +// Parameters: +// - []byte - A JSON marshalled channel ID ([id.ID]). This may be retrieved +// using ChannelsManager.GetChannelId. +func (cm *ChannelsManager) LeaveChannel(marshalledChanId []byte) error { + // Unmarshal channel ID + channelId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return err + } + + // Leave the channel + return cm.api.LeaveChannel(channelId) +} + +// ReplayChannel replays all messages from the channel within the network's +// memory (~3 weeks) over the event model. +// +// Parameters: +// - []byte - A JSON marshalled channel ID ([id.ID]). This may be retrieved +// using ChannelsManager.GetChannelId. +func (cm *ChannelsManager) ReplayChannel(marshalledChanId []byte) error { + + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return err + } + + // Replay channel + return cm.api.ReplayChannel(chanId) +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Sending Methods & Reports // +//////////////////////////////////////////////////////////////////////////////// + +// ChannelSendReport is the bindings' representation of the return values of +// ChannelsManager's Send operations. +// +// JSON Example: +// { +// "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=", +// "RoundId": { +// "Rounds": [ +// 123 +// ] +// }, +// "EphId": 0 +// } +type ChannelSendReport struct { + MessageId []byte + RoundId RoundsList + EphId int64 +} + +// SendGeneric is used to send a raw message over a channel. In general, it +// should be wrapped in a function which 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 isn't +// 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 +// Them meaning of validUntil depends on the use case. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). This may +// be retrieved using ChannelsManager.GetChannelId. +// - messageType - The message type of the message. This will be a valid +// [channels.MessageType]. +// - message - The contents of the message. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in MS. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// todo: should enumerate use cases +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be +// empty, and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport. +func (cm *ChannelsManager) SendGeneric(marshalledChanId []byte, + messageType int, message []byte, leaseTimeMS int64, + cmixParamsJSON []byte) ([]byte, error) { + + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // If passed in empty params, use the default + if len(cmixParamsJSON) == 0 { + jww.WARN.Printf("cMix params not specified, using defaults...") + cmixParamsJSON = GetDefaultCMixParams() + } + + // Unmarshal cmix params + params, err := parseCMixParams(cmixParamsJSON) + if err != nil { + return nil, err + } + + // Send message + chanMsgId, rndId, ephId, err := cm.api.SendGeneric(chanId, + channels.MessageType(messageType), message, + time.Duration(leaseTimeMS), params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + chanSendReport := ChannelSendReport{ + MessageId: chanMsgId.Bytes(), + RoundId: makeRoundsList(rndId), + EphId: ephId.Int64(), + } + + // Marshal send report + return json.Marshal(chanSendReport) +} + +// 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 which 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. +// +// Parameters: +// - adminPrivateKey - The PEM-encoded admin's RSA private key. +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). This may +// be retrieved using ChannelsManager.GetChannelId. +// - messageType - The message type of the message. This will be a valid +// [channels.MessageType]. +// - message - The contents of the message. The message should be at most 510 +// bytes. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in MS. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// todo: should enumerate use cases +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be +// empty, and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport. +func (cm *ChannelsManager) SendAdminGeneric(adminPrivateKey, + marshalledChanId []byte, + messageType int, message []byte, leaseTimeMS int64, + cmixParamsJSON []byte) ([]byte, error) { + + // Load private key from file + rsaPrivKey, err := rsa.LoadPrivateKeyFromPem(adminPrivateKey) + if err != nil { + return nil, err + } + + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // If passed in empty params, use the default + if len(cmixParamsJSON) == 0 { + jww.WARN.Printf("cMix params not specified, using defaults...") + cmixParamsJSON = GetDefaultCMixParams() + } + + // Unmarshal cmix params + params, err := parseCMixParams(cmixParamsJSON) + if err != nil { + return nil, err + } + + // Send admin message + chanMsgId, rndId, ephId, err := cm.api.SendAdminGeneric(rsaPrivKey, + chanId, channels.MessageType(messageType), message, + time.Duration(leaseTimeMS), params.CMIX) + + // Construct send report + chanSendReport := ChannelSendReport{ + MessageId: chanMsgId.Bytes(), + RoundId: makeRoundsList(rndId), + EphId: ephId.Int64(), + } + + return json.Marshal(chanSendReport) +} + +// 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: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). This may +// be retrieved using ChannelsManager.GetChannelId. +// - message - The contents of the message. The message should be at most 510 +// bytes. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in MS. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// todo: should enumerate use cases +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be +// empty, and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport +func (cm *ChannelsManager) SendMessage(marshalledChanId []byte, + message []byte, leaseTimeMS int64, cmixParamsJSON []byte) ([]byte, error) { + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // If passed in empty params, use the default + if len(cmixParamsJSON) == 0 { + jww.WARN.Printf("cMix params not specified, using defaults...") + cmixParamsJSON = GetDefaultCMixParams() + } + + // Unmarshal cmix params + params, err := parseCMixParams(cmixParamsJSON) + if err != nil { + return nil, err + } + + // Send message fixme: why is message a string here? + chanMsgId, rndId, ephId, err := cm.api.SendMessage(chanId, string(message), + time.Duration(leaseTimeMS), params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + chanSendReport := ChannelSendReport{ + MessageId: chanMsgId.Bytes(), + RoundId: makeRoundsList(rndId), + EphId: ephId.Int64(), + } + + // Marshal send report + return json.Marshal(chanSendReport) +} + +// SendReply is used to send a formatted message over a channel. +// Due to the underlying encoding using compression, it isn't +// possible to define the largest payload that can be sent, but +// it will always be possible to send a payload of 766 bytes at minimum. +// If the message ID the reply is sent to does not exist, the other side will +// post the message as a normal message and not a reply. +// The message will auto delete validUntil after the round it is sent in, +// lasting forever if ValidForever is used. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). This may +// be retrieved using ChannelsManager.GetChannelId. +// - message - The contents of the message. The message should be at most 510 +// bytes. +// - messageToReactTo - The marshalled [channel.MessageID] of the message +// you wish to reply to. This may be found in the ChannelSendReport if +// replying to your own. Alternatively, if reacting to another user's +// message, you may retrieve it via the ChannelMessageReceptionCallback +// registered using RegisterReceiveHandler. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in MS. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// todo: should enumerate use cases +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be +// empty, and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport +func (cm *ChannelsManager) SendReply(marshalledChanId []byte, + message []byte, messageToReactTo []byte, leaseTimeMS int64, + cmixParamsJSON []byte) ([]byte, error) { + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // If passed in empty params, use the default + if len(cmixParamsJSON) == 0 { + jww.WARN.Printf("cMix params not specified, using defaults...") + cmixParamsJSON = GetDefaultCMixParams() + } + + // Unmarshal cmix params + params, err := parseCMixParams(cmixParamsJSON) + if err != nil { + return nil, err + } + + // Unmarshal message ID + msgId := cryptoChannel.MessageID{} + copy(msgId[:], messageToReactTo) + + // Send Reply fixme: why is message a string here? + chanMsgId, rndId, ephId, err := cm.api.SendReply(chanId, string(message), + msgId, time.Duration(leaseTimeMS), params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + chanSendReport := ChannelSendReport{ + MessageId: chanMsgId.Bytes(), + RoundId: makeRoundsList(rndId), + EphId: ephId.Int64(), + } + + // Marshal send reportleaseTimeMS int + return json.Marshal(chanSendReport) +} + +// 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. +// Users will drop the reaction if they do not recognize the reactTo message. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). This may +// be retrieved using ChannelsManager.GetChannelId. +// - reaction - The user's reaction. This should be a single emoji with no +// other characters. +// - messageToReactTo - The marshalled [channel.MessageID] of the message +// you wish to reply to. This may be found in the ChannelSendReport if +// replying to your own. Alternatively, if reacting to another user's +// message, you may retrieve it via the ChannelMessageReceptionCallback +// registered using RegisterReceiveHandler. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in MS. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// todo: should enumerate use cases +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be +// empty, and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport +func (cm *ChannelsManager) SendReaction(marshalledChanId []byte, + reaction []byte, messageToReactTo []byte, + cmixParamsJSON []byte) ([]byte, error) { + + //Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // If passed in empty params, use the default + if len(cmixParamsJSON) == 0 { + jww.WARN.Printf("cMix params not specified, using defaults...") + cmixParamsJSON = GetDefaultCMixParams() + } + + // Unmarshal cmix params + params, err := parseCMixParams(cmixParamsJSON) + if err != nil { + return nil, err + } + + // Unmarshal message ID + msgId := cryptoChannel.MessageID{} + copy(msgId[:], messageToReactTo) + + // Send reaction + chanMsgId, rndId, ephId, err := cm.api.SendReaction(chanId, + string(reaction), msgId, params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + chanSendReport := ChannelSendReport{ + MessageId: chanMsgId.Bytes(), + RoundId: makeRoundsList(rndId), + EphId: ephId.Int64(), + } + + // Marshal send report + return json.Marshal(chanSendReport) +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Receiving Logic & Callback Registration // +//////////////////////////////////////////////////////////////////////////////// + +// ReceivedChannelMessage is a report structure returned via the +// ChannelMessageReceptionCallback. This report gives the context +// for the channel the message was sent to and the message itself. +// This is returned via the callback as JSON marshalled bytes. +// +// Example JSON: +// { +// "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +// "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=", +// "MessageType": 42, +// "SenderUsername": "hunter2", +// "Content": "YmFuX2JhZFVTZXI=", +// "Timestamp": 1662502150335283000, +// "Lease": 25, +// "RoundIds": { +// "Rounds": [ +// 11420 +// ] +// } +// } +type ReceivedChannelMessage struct { + ChannelId []byte + MessageId []byte + MessageType int + SenderUsername string + Content []byte + Timestamp int64 + Lease int64 + RoundIds RoundsList +} + +// ChannelMessageReceptionCallback is the callback that returns the +// context for a channel message via the Callback. +type ChannelMessageReceptionCallback interface { + Callback(receivedChannelMessageReport []byte, err error) +} + +// 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: +// - messageType - represents the [channels.MessageType] which will +// have a registered listener +// - listenerCb - the callback which will be executed when a channel message +// of messageType is received. +func (cm *ChannelsManager) RegisterReceiveHandler(messageType int, + listenerCb ChannelMessageReceptionCallback) error { + + // Wrap callback around backend interface + cb := channels.MessageTypeReceiveMessage( + func(channelID *id.ID, messageID cryptoChannel.MessageID, + messageType channels.MessageType, senderUsername string, + content []byte, timestamp time.Time, lease time.Duration, + round rounds.Round) { + + rcm := ReceivedChannelMessage{ + ChannelId: channelID.Marshal(), + MessageId: messageID.Bytes(), + MessageType: int(messageType), + SenderUsername: senderUsername, + Content: content, + Timestamp: timestamp.UnixNano(), + Lease: int64(lease), + RoundIds: makeRoundsList(round.ID), + } + + listenerCb.Callback(json.Marshal(rcm)) + }) + + // Register handler + return cm.api.RegisterReceiveHandler(channels.MessageType(messageType), cb) +}