diff --git a/channels/emoji.go b/channels/emoji.go new file mode 100644 index 0000000000000000000000000000000000000000..f97d50889ccb3f931ee33b22f970b5b8747b9465 --- /dev/null +++ b/channels/emoji.go @@ -0,0 +1,6 @@ +package channels + +// ValidateReaction checks that the reaction only contains a single Emoji +func ValidateReaction(reaction string) error { + +} diff --git a/channels/eventModel.go b/channels/eventModel.go index f86ec23e14ff8ca96574be95ddb65d3a5f03bc4c..08eb972610026dfd8770aabe55fa5f0f01e078ca 100644 --- a/channels/eventModel.go +++ b/channels/eventModel.go @@ -2,6 +2,7 @@ package channels import ( "errors" + "github.com/golang/protobuf/proto" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/primitives/states" @@ -26,22 +27,23 @@ type EventModel interface { LeaveChannel(ChannelID *id.ID) ReceiveMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, - SenderUsername string, Content []byte, + SenderUsername string, text string, timestamp time.Time, lease time.Duration, round rounds.Round) ReceiveReply(ChannelID *id.ID, MessageID cryptoChannel.MessageID, ReplyTo cryptoChannel.MessageID, SenderUsername string, - Content []byte, timestamp time.Time, lease time.Duration, + text string, timestamp time.Time, lease time.Duration, round rounds.Round) ReceiveReaction(ChannelID *id.ID, MessageID cryptoChannel.MessageID, ReactionTo cryptoChannel.MessageID, SenderUsername string, Reaction []byte, timestamp time.Time, lease time.Duration, round rounds.Round) - IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) - UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) - PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time) - UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + //unimplemented + //IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + //UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + //PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time) + //UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) } type MessageTypeReceiveMessage func(ChannelID *id.ID, @@ -63,8 +65,9 @@ func initEvents(model EventModel) *events { } //set up default message types - e.registered[Text] = e.model.ReceiveTextMessage - e.registered[AdminText] = e.model.ReceiveAdminTextMessage + e.registered[Text] = e.receiveTextMessage + e.registered[AdminText] = e.receiveTextMessage + e.registered[Reaction] = return e } @@ -127,3 +130,70 @@ func (e *events) triggerAdminEvent(chID *id.ID, cm *ChannelMessage, cm.Payload, round.Timestamps[states.QUEUED], time.Duration(cm.Lease), round) return } + +func (e *events) receiveTextMessage(ChannelID *id.ID, + MessageID cryptoChannel.MessageID, messageType MessageType, + SenderUsername string, Content []byte, timestamp time.Time, + lease time.Duration, round rounds.Round) { + txt := &CMIXChannelText{} + if err := proto.Unmarshal(Content, txt); err != nil { + jww.ERROR.Printf("Failed to text unmarshal message %s from %s on "+ + "channel %s, type %s, ts: %s, lease: %s, round: %d: %+v", + MessageID, SenderUsername, ChannelID, messageType, timestamp, lease, + round.ID, err) + return + } + + if txt.ReplyMessageID != nil { + if len(txt.ReplyMessageID) == cryptoChannel.MessageIDLen { + var replyTo cryptoChannel.MessageID + copy(replyTo[:], txt.ReplyMessageID) + e.model.ReceiveReply(ChannelID, MessageID, replyTo, SenderUsername, txt.Text, + timestamp, lease, round) + return + + } else { + jww.ERROR.Printf("Failed process reply to for message %s from %s on "+ + "channel %s, type %s, ts: %s, lease: %s, round: %d, returning "+ + "without reply", + MessageID, SenderUsername, ChannelID, messageType, timestamp, lease, + round.ID) + } + } + + e.model.ReceiveMessage(ChannelID, MessageID, SenderUsername, txt.Text, + timestamp, lease, round) +} + +func (e *events) receiveReaction(ChannelID *id.ID, + MessageID cryptoChannel.MessageID, messageType MessageType, + SenderUsername string, Content []byte, timestamp time.Time, + lease time.Duration, round rounds.Round) { + react := &CMIXChannelReaction{} + if err := proto.Unmarshal(Content, react); err != nil { + jww.ERROR.Printf("Failed to text unmarshal message %s from %s on "+ + "channel %s, type %s, ts: %s, lease: %s, round: %d: %+v", + MessageID, SenderUsername, ChannelID, messageType, timestamp, lease, + round.ID, err) + return + } + + if react.ReactionMessageID != nil && len(react.ReactionMessageID) == cryptoChannel.MessageIDLen { + var reactTo cryptoChannel.MessageID + copy(replyTo[:], react.ReactionMessageID) + e.model.ReceiveReply(ChannelID, MessageID, replyTo, SenderUsername, txt.Text, + timestamp, lease, round) + return + + } else { + jww.ERROR.Printf("Failed process reply to for message %s from %s on "+ + "channel %s, type %s, ts: %s, lease: %s, round: %d, returning "+ + "without reply", + MessageID, SenderUsername, ChannelID, messageType, timestamp, lease, + round.ID) + } +} + +e.model.ReceiveMessage(ChannelID, MessageID, SenderUsername, txt.Text, +timestamp, lease, round) +} \ No newline at end of file diff --git a/channels/interface.go b/channels/interface.go index 8ecc115e96c73d7e8087e692a677fcc9fdcfd56d..f3a62ca7a49cb8c37b43393d2acf8824ab021a1a 100644 --- a/channels/interface.go +++ b/channels/interface.go @@ -27,7 +27,7 @@ type Manager interface { SendReply(channelID *id.ID, msg string, replyTo cryptoChannel.MessageID, validUntil time.Duration, params cmix.CMIXParams) ( cryptoChannel.MessageID, id.Round, ephemeral.Id, error) - SendReaction(channelID *id.ID, msg []byte, + SendReaction(channelID *id.ID, reaction string, reactTo cryptoChannel.MessageID, validUntil time.Duration, params cmix.CMIXParams) ( cryptoChannel.MessageID, id.Round, ephemeral.Id, error) } diff --git a/channels/joinedChannel.go b/channels/joinedChannel.go index 0936b40eac894e6806d87ddc0faa77c230eafc42..81c34bb4884fab649b68c69f95e833c648947797 100644 --- a/channels/joinedChannel.go +++ b/channels/joinedChannel.go @@ -123,6 +123,22 @@ func (m *manager) addChannel(channel cryptoBroadcast.Channel) error { return nil } +func (m *manager) removeChannel(channelId *id.ID) error { + m.mux.Lock() + defer m.mux.Unlock() + + ch, exists := m.channels[channelId] + if !exists { + return ChannelDoesNotExistsErr + } + + ch.broadcast.Stop() + + delete(m.channels, channelId) + + return nil +} + //getChannel returns the given channel, if it exists func (m *manager) getChannel(channelId *id.ID) (*joinedChannel, error) { m.mux.RLock() diff --git a/channels/manager.go b/channels/manager.go index 43ffa87b6de5ca4d574edac9064b906fce828bc2..bf8f820b671f9dfa4d4fc83bb753eff56a599b28 100644 --- a/channels/manager.go +++ b/channels/manager.go @@ -1,22 +1,14 @@ package channels import ( - "github.com/golang/protobuf/proto" "gitlab.com/elixxir/client/broadcast" - "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/storage/versioned" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" - cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/fastRNG" - "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" - "gitlab.com/xx_network/primitives/id/ephemeral" "sync" - "time" ) -const cmixChannelTextVerion = 0 - type manager struct { //List of all channels channels map[*id.ID]*joinedChannel @@ -38,187 +30,19 @@ func NewManager() { } func (m *manager) JoinChannel(channel cryptoBroadcast.Channel) error { - return m.addChannel(channel) -} - -func (m *manager) SendGeneric(channelID *id.ID, msg []byte, validUntil time.Duration, - messageType MessageType, params cmix.CMIXParams) (cryptoChannel.MessageID, - id.Round, ephemeral.Id, error) { - - //find the channel - ch, err := m.getChannel(channelID) + err := m.addChannel(channel) if err != nil { - return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + return err } - - var msgId cryptoChannel.MessageID - //Note: we are not checking check if message is too long before trying to - //find a round - - //Build the function pointer that will build the message - assemble := func(rid id.Round) ([]byte, error) { - - //Build the message - chMsg := &ChannelMessage{ - Lease: validUntil.Nanoseconds(), - RoundID: uint64(rid), - PayloadType: uint32(messageType), - Payload: msg, - } - - //Serialize the message - chMsgSerial, err := proto.Marshal(chMsg) - if err != nil { - return nil, err - } - - //Sign the message - messageSig, err := m.name.SignChannelMessage(chMsgSerial) - if err != nil { - return nil, err - } - - //Build the user message - validationSig, unameLease := m.name.GetChannelValidationSignature() - - usrMsg := &UserMessage{ - Message: chMsgSerial, - ValidationSignature: validationSig, - Signature: messageSig, - Username: m.name.GetUsername(), - ECCPublicKey: m.name.GetChannelPubkey(), - UsernameLease: unameLease.Unix(), - } - - //Serialize the user message - usrMsgSerial, err := proto.Marshal(usrMsg) - if err != nil { - return nil, err - } - - //Fill in any extra bits in the payload to ensure it is the right size - usrMsgSerialSized, err := broadcast.NewSizedBroadcast( - ch.broadcast.MaxAsymmetricPayloadSize(), usrMsgSerial) - if err != nil { - return nil, err - } - - msgId = cryptoChannel.MakeMessageID(usrMsgSerialSized) - - return usrMsgSerialSized, nil - } - - //TODO: send the send message over to reception manually so it is added to - //the database early - rid, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params) - return msgId, rid, ephid, err + go m.events.model.JoinChannel(channel) + return nil } -func (m *manager) SendAdminGeneric(privKey *rsa.PrivateKey, channelID *id.ID, - msg []byte, validUntil time.Duration, messageType MessageType, - params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id, - error) { - - //find the channel - ch, err := m.getChannel(channelID) +func (m *manager) LeaveChannel(channelId *id.ID) error { + err := m.removeChannel(channelId) if err != nil { - return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + return err } - - var msgId cryptoChannel.MessageID - //Note: we are not checking check if message is too long before trying to - //find a round - - //Build the function pointer that will build the message - assemble := func(rid id.Round) ([]byte, error) { - - //Build the message - chMsg := &ChannelMessage{ - Lease: validUntil.Nanoseconds(), - RoundID: uint64(rid), - PayloadType: uint32(messageType), - Payload: msg, - } - - //Serialize the message - chMsgSerial, err := proto.Marshal(chMsg) - if err != nil { - return nil, err - } - - //check if the message is too long - if len(chMsgSerial) > broadcast.MaxSizedBroadcastPayloadSize(privKey.Size()) { - return nil, MessageTooLongErr - } - - //Fill in any extra bits in the payload to ensure it is the right size - chMsgSerialSized, err := broadcast.NewSizedBroadcast( - ch.broadcast.MaxAsymmetricPayloadSize(), chMsgSerial) - if err != nil { - return nil, err - } - - msgId = cryptoChannel.MakeMessageID(chMsgSerialSized) - - return chMsgSerialSized, nil - } - - //TODO: send the send message over to reception manually so it is added to - //the database early - rid, ephid, err := ch.broadcast.BroadcastAsymmetricWithAssembler(privKey, - assemble, params) - return msgId, rid, ephid, err -} - -func (m *manager) SendMessage(channelID *id.ID, msg string, - validUntil time.Duration, params cmix.CMIXParams) ( - cryptoChannel.MessageID, id.Round, ephemeral.Id, error) { - txt := &CMIXChannelText{ - Version: cmixChannelTextVerion, - Text: msg, - ReplyMessageID: nil, - } - - txtMarshaled, err := proto.Marshal(txt) - if err != nil { - return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err - } - - return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params) -} - -func (m *manager) SendReply(channelID *id.ID, msg string, - replyTo cryptoChannel.MessageID, validUntil time.Duration, - params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id, - error) { - txt := &CMIXChannelText{ - Version: cmixChannelTextVerion, - Text: msg, - ReplyMessageID: replyTo[:], - } - - txtMarshaled, err := proto.Marshal(txt) - if err != nil { - return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err - } - - return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params) -} - -func (m *manager) SendReaction(channelID *id.ID, msg string, - replyTo cryptoChannel.MessageID, validUntil time.Duration, - params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id, - error) { - txt := &CMIXChannelText{ - Version: cmixChannelTextVerion, - Text: msg, - ReplyMessageID: replyTo[:], - } - - txtMarshaled, err := proto.Marshal(txt) - if err != nil { - return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err - } - - return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params) + go m.events.model.LeaveChannel(channelId) + return nil } diff --git a/channels/send.go b/channels/send.go index d72c017e84aa8688cf59f7a04353b0f96da67b06..34dfeef454c1fb1484741553ddf30ae38915d76d 100644 --- a/channels/send.go +++ b/channels/send.go @@ -1 +1,205 @@ package channels + +import ( + "github.com/forPelevin/gomoji" + "gitlab.com/elixxir/client/broadcast" + "gitlab.com/elixxir/client/cmix" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "google.golang.org/protobuf/proto" + "time" +) + +const ( + cmixChannelTextVersion = 0 + cmixChannelReactionVersion = 0 +) + +func (m *manager) SendGeneric(channelID *id.ID, msg []byte, validUntil time.Duration, + messageType MessageType, params cmix.CMIXParams) (cryptoChannel.MessageID, + id.Round, ephemeral.Id, error) { + + //find the channel + ch, err := m.getChannel(channelID) + if err != nil { + return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + } + + var msgId cryptoChannel.MessageID + //Note: we are not checking check if message is too long before trying to + //find a round + + //Build the function pointer that will build the message + assemble := func(rid id.Round) ([]byte, error) { + + //Build the message + chMsg := &ChannelMessage{ + Lease: validUntil.Nanoseconds(), + RoundID: uint64(rid), + PayloadType: uint32(messageType), + Payload: msg, + } + + //Serialize the message + chMsgSerial, err := proto.Marshal(chMsg) + if err != nil { + return nil, err + } + + //Sign the message + messageSig, err := m.name.SignChannelMessage(chMsgSerial) + if err != nil { + return nil, err + } + + //Build the user message + validationSig, unameLease := m.name.GetChannelValidationSignature() + + usrMsg := &UserMessage{ + Message: chMsgSerial, + ValidationSignature: validationSig, + Signature: messageSig, + Username: m.name.GetUsername(), + ECCPublicKey: m.name.GetChannelPubkey(), + UsernameLease: unameLease.Unix(), + } + + //Serialize the user message + usrMsgSerial, err := proto.Marshal(usrMsg) + if err != nil { + return nil, err + } + + //Fill in any extra bits in the payload to ensure it is the right size + usrMsgSerialSized, err := broadcast.NewSizedBroadcast( + ch.broadcast.MaxAsymmetricPayloadSize(), usrMsgSerial) + if err != nil { + return nil, err + } + + msgId = cryptoChannel.MakeMessageID(usrMsgSerialSized) + + return usrMsgSerialSized, nil + } + + //TODO: send the send message over to reception manually so it is added to + //the database early + rid, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params) + return msgId, rid, ephid, err +} + +func (m *manager) SendAdminGeneric(privKey *rsa.PrivateKey, channelID *id.ID, + msg []byte, validUntil time.Duration, messageType MessageType, + params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id, + error) { + + //find the channel + ch, err := m.getChannel(channelID) + if err != nil { + return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + } + + var msgId cryptoChannel.MessageID + //Note: we are not checking check if message is too long before trying to + //find a round + + //Build the function pointer that will build the message + assemble := func(rid id.Round) ([]byte, error) { + + //Build the message + chMsg := &ChannelMessage{ + Lease: validUntil.Nanoseconds(), + RoundID: uint64(rid), + PayloadType: uint32(messageType), + Payload: msg, + } + + //Serialize the message + chMsgSerial, err := proto.Marshal(chMsg) + if err != nil { + return nil, err + } + + //check if the message is too long + if len(chMsgSerial) > broadcast.MaxSizedBroadcastPayloadSize(privKey.Size()) { + return nil, MessageTooLongErr + } + + //Fill in any extra bits in the payload to ensure it is the right size + chMsgSerialSized, err := broadcast.NewSizedBroadcast( + ch.broadcast.MaxAsymmetricPayloadSize(), chMsgSerial) + if err != nil { + return nil, err + } + + msgId = cryptoChannel.MakeMessageID(chMsgSerialSized) + + return chMsgSerialSized, nil + } + + //TODO: send the send message over to reception manually so it is added to + //the database early + rid, ephid, err := ch.broadcast.BroadcastAsymmetricWithAssembler(privKey, + assemble, params) + return msgId, rid, ephid, err +} + +func (m *manager) SendMessage(channelID *id.ID, msg string, + validUntil time.Duration, params cmix.CMIXParams) ( + cryptoChannel.MessageID, id.Round, ephemeral.Id, error) { + txt := &CMIXChannelText{ + Version: cmixChannelTextVersion, + Text: msg, + ReplyMessageID: nil, + } + + txtMarshaled, err := proto.Marshal(txt) + if err != nil { + return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + } + + return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params) +} + +func (m *manager) SendReply(channelID *id.ID, msg string, + replyTo cryptoChannel.MessageID, validUntil time.Duration, + params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id, + error) { + txt := &CMIXChannelText{ + Version: cmixChannelTextVersion, + Text: msg, + ReplyMessageID: replyTo[:], + } + + txtMarshaled, err := proto.Marshal(txt) + if err != nil { + return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + } + + return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params) +} + +func (m *manager) SendReaction(channelID *id.ID, reaction string, + replyTo cryptoChannel.MessageID, validUntil time.Duration, + params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id, + error) { + + if len(reaction) != 1 { + return error + } + + txt := &CMIXChannelReaction{ + Version: cmixChannelReactionVersion, + Reaction: reaction, + ReactionMessageID: replyTo[:], + } + + txtMarshaled, err := proto.Marshal(txt) + if err != nil { + return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err + } + + return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params) +} diff --git a/go.mod b/go.mod index c783049e4a394de789f9bd4d72ff01cd7fc25eb6..4864ab7a9547d35ab4967a01dcce25428fe867f0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/cloudflare/circl v1.2.0 + github.com/forPelevin/gomoji v1.1.6 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang/protobuf v1.5.2 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 @@ -39,6 +40,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/rivo/uniseg v0.3.4 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect diff --git a/go.sum b/go.sum index 15026f41da3db123c76448fdd513095d108f0814..3898f13b2d9af37a8e82268407d8423e1badb09a 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/forPelevin/gomoji v1.1.6 h1:mSIGhjyMiywuGFHR/6CLL/L6HwwDiQmYGdl1R9a/05w= +github.com/forPelevin/gomoji v1.1.6/go.mod h1:h31zCiwG8nIto/c9RmijODA1xgN2JSvwKfU7l65xeTk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -358,6 +360,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= +github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=