diff --git a/cmd/status-cli/message.go b/cmd/status-cli/message.go index 519b214cf..005edff40 100644 --- a/cmd/status-cli/message.go +++ b/cmd/status-cli/message.go @@ -3,6 +3,7 @@ package main import ( "bufio" "context" + "log/slog" "os" "strings" "sync" @@ -12,17 +13,15 @@ import ( "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" - - "github.com/urfave/cli/v2" ) -func (cli *StatusCLI) sendContactRequest(cCtx *cli.Context, toID string) error { +func (cli *StatusCLI) sendContactRequest(ctx context.Context, toID string) error { cli.logger.Info("send contact request, contact public key: ", toID) request := &requests.SendContactRequest{ ID: toID, Message: "Hello!", } - resp, err := cli.messenger.SendContactRequest(cCtx.Context, request) + resp, err := cli.messenger.SendContactRequest(ctx, request) cli.logger.Info("function SendContactRequest response.messages: ", resp.Messages()) if err != nil { return err @@ -31,9 +30,9 @@ func (cli *StatusCLI) sendContactRequest(cCtx *cli.Context, toID string) error { return nil } -func (cli *StatusCLI) sendContactRequestAcceptance(cCtx *cli.Context, msgID string) error { +func (cli *StatusCLI) sendContactRequestAcceptance(ctx context.Context, msgID string) error { cli.logger.Info("accept contact request, message ID: ", msgID) - resp, err := cli.messenger.AcceptContactRequest(cCtx.Context, &requests.AcceptContactRequest{ID: types.Hex2Bytes(msgID)}) + resp, err := cli.messenger.AcceptContactRequest(ctx, &requests.AcceptContactRequest{ID: types.Hex2Bytes(msgID)}) if err != nil { return err } @@ -102,48 +101,75 @@ func (cli *StatusCLI) retrieveMessagesLoop(ctx context.Context, tick time.Durati } } -func (cli *StatusCLI) sendMessageLoop(ctx context.Context, tick time.Duration, wg *sync.WaitGroup, sem chan struct{}, cancel context.CancelFunc) { - defer wg.Done() - - ticker := time.NewTicker(tick) - defer ticker.Stop() - +// interactiveSendMessageLoop reads input from stdin and sends it as a direct message to the first mutual contact. +// +// If multiple CLIs are provided, it will send messages in a round-robin fashion: +// 1st input message will be from Alice, 2nd from Bob, 3rd from Alice, and so on. +func interactiveSendMessageLoop(ctx context.Context, clis ...*StatusCLI) { reader := bufio.NewReader(os.Stdin) + i := -1 + n := len(clis) + if n == 0 { + slog.Error("at least 1 CLI needed") + return + } for { - select { - case <-ticker.C: - if len(cli.messenger.MutualContacts()) == 0 { - continue - } - sem <- struct{}{} - cli.logger.Info("Enter your message to send: (type 'quit' or 'q' to exit)") - message, err := reader.ReadString('\n') - if err != nil { - <-sem - cli.logger.Error("failed to read input", err) - continue - } + i++ + if i >= n { + i = 0 + } + cli := clis[i] // round robin cli selection - message = strings.TrimSpace(message) - if message == "quit" || message == "q" || strings.Contains(message, "\x03") { - cancel() - <-sem + if len(cli.messenger.MutualContacts()) == 0 { + // waits for 1 second before trying again + time.Sleep(1 * time.Second) + continue + } + cli.logger.Info("Enter your message to send: (type 'quit' or 'q' to exit)") + + message, err := readInput(ctx, reader) + if err != nil { + if err == context.Canceled { return } - if message == "" { - <-sem - continue - } - - err = cli.sendDirectMessage(ctx, message) - time.Sleep(WaitingInterval) - <-sem - if err != nil { - cli.logger.Error("failed to send direct message: ", err) - continue - } - case <-ctx.Done(): + cli.logger.Error("failed to read input", err) + continue + } + message = strings.TrimSpace(message) + if message == "quit" || message == "q" || strings.Contains(message, "\x03") { return } + if message == "" { + continue + } + if err = cli.sendDirectMessage(ctx, message); err != nil { + cli.logger.Error("failed to send direct message: ", err) + continue + } + } +} + +// readInput reads input from the reader and respects context cancellation +func readInput(ctx context.Context, reader *bufio.Reader) (string, error) { + inputCh := make(chan string, 1) + errCh := make(chan error, 1) + + // Start a goroutine to read input + go func() { + input, err := reader.ReadString('\n') + if err != nil { + errCh <- err + return + } + inputCh <- input + }() + + select { + case <-ctx.Done(): + return "", ctx.Err() + case input := <-inputCh: + return input, nil + case err := <-errCh: + return "", err } } diff --git a/cmd/status-cli/serve.go b/cmd/status-cli/serve.go index 75117303b..29be29d39 100644 --- a/cmd/status-cli/serve.go +++ b/cmd/status-cli/serve.go @@ -2,26 +2,21 @@ package main import ( "context" + "encoding/json" "log" "os" "os/signal" - "sync" "syscall" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" + msignal "github.com/status-im/status-go/signal" + "github.com/urfave/cli/v2" "go.uber.org/zap" ) func serve(cCtx *cli.Context) error { - ctx, cancel := context.WithCancel(cCtx.Context) - - go func() { - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - <-sig - cancel() - }() - rawLogger, err := zap.NewDevelopment() if err != nil { log.Fatalf("Error initializing logger: %v", err) @@ -38,44 +33,69 @@ func serve(cCtx *cli.Context) error { apiModules := cCtx.String(APIModulesFlag) telemetryUrl := cCtx.String(TelemetryServerURLFlag) - cli, err := start(cCtx, name, port, apiModules, telemetryUrl) + cli, err := start(name, port, apiModules, telemetryUrl) if err != nil { return err } defer cli.stop() - // Retrieve for messages - var wg sync.WaitGroup - msgCh := make(chan string) + // Using the mobile signal handler to listen for received messages + // because if we call messenger.RetrieveAll() from different routines we will miss messages in one of them + // and the retrieve messages loop is started when starting a node, so we needed a different appproach, + // alternatively we could have implemented another notification mechanism in the messenger, but this signal is already in place + msignal.SetMobileSignalHandler(msignal.MobileSignalHandler(func(s []byte) { + var ev MobileSignalEvent + if err := json.Unmarshal(s, &ev); err != nil { + logger.Errorf("unmarshaling signal event: %v", err) + return + } - wg.Add(1) - go cli.retrieveMessagesLoop(ctx, RetrieveInterval, msgCh, &wg) + if ev.Type == msignal.EventNewMessages { + for _, message := range ev.Event.Messages { + logger.Infof("message received: %v (ID=%v)", message.Text, message.ID) + // if request contact, accept it + if message.ContentType == protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT { + if err = cli.sendContactRequestAcceptance(cCtx.Context, message.ID); err != nil { + logger.Errorf("accepting contact request: %v", err) + return + } + } + } + } + })) - // Send and accept contact request + // Send contact request dest := cCtx.String(AddFlag) if dest != "" { - err := cli.sendContactRequest(cCtx, dest) + err := cli.sendContactRequest(cCtx.Context, dest) if err != nil { return err } } + // nightly testrunner looks for this log to consider node as started + logger.Info("retrieve messages...") + + ctx, cancel := context.WithCancel(cCtx.Context) go func() { - msgID := <-msgCh - err = cli.sendContactRequestAcceptance(cCtx, msgID) - if err != nil { - logger.Error(err) - return - } + // Wait for signal to exit + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + cancel() }() // Send message if mutual contact exists - sem := make(chan struct{}, 1) - wg.Add(1) - go cli.sendMessageLoop(ctx, SendInterval, &wg, sem, cancel) + interactiveSendMessageLoop(ctx, cli) - wg.Wait() logger.Info("Exiting") return nil } + +type MobileSignalEvent struct { + Type string `json:"type"` + Event struct { + Messages []*common.Message `json:"messages"` + } `json:"event"` +} diff --git a/cmd/status-cli/simulate.go b/cmd/status-cli/simulate.go index 15aee6978..35b895933 100644 --- a/cmd/status-cli/simulate.go +++ b/cmd/status-cli/simulate.go @@ -39,13 +39,13 @@ func simulate(cCtx *cli.Context) error { apiModules := cCtx.String(APIModulesFlag) telemetryUrl := cCtx.String(TelemetryServerURLFlag) - alice, err := start(cCtx, "Alice", 0, apiModules, telemetryUrl) + alice, err := start("Alice", 0, apiModules, telemetryUrl) if err != nil { return err } defer alice.stop() - charlie, err := start(cCtx, "Charlie", 0, apiModules, telemetryUrl) + charlie, err := start("Charlie", 0, apiModules, telemetryUrl) if err != nil { return err } @@ -63,27 +63,23 @@ func simulate(cCtx *cli.Context) error { // Send contact request from Alice to Charlie, charlie accept the request time.Sleep(WaitingInterval) destID := charlie.messenger.GetSelfContact().ID - err = alice.sendContactRequest(cCtx, destID) + err = alice.sendContactRequest(ctx, destID) if err != nil { return err } msgID := <-msgCh - err = charlie.sendContactRequestAcceptance(cCtx, msgID) + err = charlie.sendContactRequestAcceptance(ctx, msgID) if err != nil { return err } + time.Sleep(WaitingInterval) // Send DM between alice to charlie interactive := cCtx.Bool(InteractiveFlag) if interactive { - sem := make(chan struct{}, 1) - wg.Add(1) - go alice.sendMessageLoop(ctx, SendInterval, &wg, sem, cancel) - wg.Add(1) - go charlie.sendMessageLoop(ctx, SendInterval, &wg, sem, cancel) + interactiveSendMessageLoop(ctx, alice, charlie) } else { - time.Sleep(WaitingInterval) for i := 0; i < cCtx.Int(CountFlag); i++ { err = alice.sendDirectMessage(ctx, fmt.Sprintf("message from alice, number: %d", i+1)) if err != nil { diff --git a/cmd/status-cli/util.go b/cmd/status-cli/util.go index d5a8fa154..952a0b606 100644 --- a/cmd/status-cli/util.go +++ b/cmd/status-cli/util.go @@ -12,7 +12,6 @@ import ( "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/services/wakuv2ext" - "github.com/urfave/cli/v2" "go.uber.org/zap" ) @@ -34,7 +33,7 @@ func setupLogger(file string) *zap.Logger { return logutils.ZapLogger() } -func start(cCtx *cli.Context, name string, port int, apiModules string, telemetryUrl string) (*StatusCLI, error) { +func start(name string, port int, apiModules string, telemetryUrl string) (*StatusCLI, error) { namedLogger := logger.Named(name) namedLogger.Info("starting messager")