github.com/status-im/status-go@v1.1.0/cmd/status-cli/message.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"crypto/rand"
     7  	"log/slog"
     8  	"math/big"
     9  	"os"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/status-im/status-go/eth-node/types"
    15  	"github.com/status-im/status-go/protocol/common"
    16  	"github.com/status-im/status-go/protocol/protobuf"
    17  	"github.com/status-im/status-go/protocol/requests"
    18  )
    19  
    20  func (cli *StatusCLI) sendContactRequest(ctx context.Context, toID string) error {
    21  	cli.logger.Info("send contact request, contact public key: ", toID)
    22  	request := &requests.SendContactRequest{
    23  		ID:      toID,
    24  		Message: "Hello!",
    25  	}
    26  	resp, err := cli.messenger.SendContactRequest(ctx, request)
    27  	cli.logger.Info("function SendContactRequest response.messages: ", resp.Messages())
    28  	if err != nil {
    29  		return err
    30  	}
    31  
    32  	return nil
    33  }
    34  
    35  func (cli *StatusCLI) sendContactRequestAcceptance(ctx context.Context, msgID string) error {
    36  	cli.logger.Info("accept contact request, message ID: ", msgID)
    37  	resp, err := cli.messenger.AcceptContactRequest(ctx, &requests.AcceptContactRequest{ID: types.Hex2Bytes(msgID)})
    38  	if err != nil {
    39  		return err
    40  	}
    41  	cli.logger.Info("function AcceptContactRequest response: ", resp.Messages())
    42  
    43  	return nil
    44  }
    45  
    46  func (cli *StatusCLI) randomFailure() func() {
    47  	nBig, err := rand.Int(rand.Reader, big.NewInt(100))
    48  	if err != nil {
    49  		cli.logger.Error("failed to generate random number", "err", err)
    50  		return nil
    51  	}
    52  	n := nBig.Int64()
    53  	if n >= 40 {
    54  		return nil
    55  	}
    56  
    57  	cli.backend.StatusNode().WakuV2Service().SkipPublishToTopic(true)
    58  
    59  	return func() {
    60  		cli.backend.StatusNode().WakuV2Service().SkipPublishToTopic(false)
    61  	}
    62  }
    63  
    64  func (cli *StatusCLI) sendDirectMessage(ctx context.Context, text string, options ...bool) error {
    65  	randomFailure := false
    66  	if len(options) > 0 {
    67  		randomFailure = options[0]
    68  	}
    69  
    70  	if len(cli.messenger.MutualContacts()) == 0 {
    71  		return nil
    72  	}
    73  	chat := cli.messenger.Chat(cli.messenger.MutualContacts()[0].ID)
    74  	cli.logger.Info("will send message to contact: ", chat.ID)
    75  
    76  	clock, timestamp := chat.NextClockAndTimestamp(cli.messenger.GetTransport())
    77  	inputMessage := common.NewMessage()
    78  	inputMessage.ChatId = chat.ID
    79  	inputMessage.LocalChatID = chat.ID
    80  	inputMessage.Clock = clock
    81  	inputMessage.Timestamp = timestamp
    82  	inputMessage.MessageType = protobuf.MessageType_ONE_TO_ONE
    83  	inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
    84  	inputMessage.Text = text
    85  
    86  	shouldFail := false
    87  	if randomFailure {
    88  		if postFailure := cli.randomFailure(); postFailure != nil {
    89  			defer postFailure()
    90  			shouldFail = true
    91  		}
    92  	}
    93  	resp, err := cli.messenger.SendChatMessage(ctx, inputMessage)
    94  	if err != nil {
    95  		if shouldFail {
    96  			cli.logger.Info("simulating message failure")
    97  			cli.logger.Error("error sending message", "err", err)
    98  			return nil
    99  		}
   100  		return err
   101  	}
   102  
   103  	for _, message := range resp.Messages() {
   104  		cli.logger.Infof("sent message: %v (ID=%v)", message.Text, message.ID)
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  func (cli *StatusCLI) retrieveMessagesLoop(ctx context.Context, tick time.Duration, msgCh chan string, wg *sync.WaitGroup) {
   111  	defer wg.Done()
   112  
   113  	ticker := time.NewTicker(tick)
   114  	defer ticker.Stop()
   115  
   116  	cli.logger.Info("retrieve messages...")
   117  
   118  	for {
   119  		select {
   120  		case <-ticker.C:
   121  			response, err := cli.messenger.RetrieveAll()
   122  			if err != nil {
   123  				cli.logger.Error("failed to retrieve raw messages", "err", err)
   124  				continue
   125  			}
   126  			if response == nil {
   127  				continue
   128  			}
   129  			for _, message := range response.Messages() {
   130  				cli.logger.Infof("message received: %v (ID=%v)", message.Text, message.ID)
   131  				if message.ContentType == protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT {
   132  					msgCh <- message.ID
   133  				}
   134  			}
   135  		case <-ctx.Done():
   136  			return
   137  		}
   138  	}
   139  }
   140  
   141  // interactiveSendMessageLoop reads input from stdin and sends it as a direct message to the first mutual contact.
   142  //
   143  // If multiple CLIs are provided, it will send messages in a round-robin fashion:
   144  // 1st input message will be from Alice, 2nd from Bob, 3rd from Alice, and so on.
   145  func interactiveSendMessageLoop(ctx context.Context, clis ...*StatusCLI) {
   146  	reader := bufio.NewReader(os.Stdin)
   147  	i := -1
   148  	n := len(clis)
   149  	if n == 0 {
   150  		slog.Error("at least 1 CLI needed")
   151  		return
   152  	}
   153  	for {
   154  		i++
   155  		if i >= n {
   156  			i = 0
   157  		}
   158  		cli := clis[i] // round robin cli selection
   159  
   160  		if len(cli.messenger.MutualContacts()) == 0 {
   161  			// waits for 1 second before trying again
   162  			time.Sleep(1 * time.Second)
   163  			continue
   164  		}
   165  		cli.logger.Info("Enter your message to send: (type 'quit' or 'q' to exit)")
   166  
   167  		message, err := readInput(ctx, reader)
   168  		if err != nil {
   169  			if err == context.Canceled {
   170  				return
   171  			}
   172  			cli.logger.Error("failed to read input", err)
   173  			continue
   174  		}
   175  		message = strings.TrimSpace(message)
   176  		if message == "quit" || message == "q" || strings.Contains(message, "\x03") {
   177  			return
   178  		}
   179  		if message == "" {
   180  			continue
   181  		}
   182  		if err = cli.sendDirectMessage(ctx, message); err != nil {
   183  			cli.logger.Error("failed to send direct message: ", err)
   184  			continue
   185  		}
   186  	}
   187  }
   188  
   189  // readInput reads input from the reader and respects context cancellation
   190  func readInput(ctx context.Context, reader *bufio.Reader) (string, error) {
   191  	inputCh := make(chan string, 1)
   192  	errCh := make(chan error, 1)
   193  
   194  	// Start a goroutine to read input
   195  	go func() {
   196  		input, err := reader.ReadString('\n')
   197  		if err != nil {
   198  			errCh <- err
   199  			return
   200  		}
   201  		inputCh <- input
   202  	}()
   203  
   204  	select {
   205  	case <-ctx.Done():
   206  		return "", ctx.Err()
   207  	case input := <-inputCh:
   208  		return input, nil
   209  	case err := <-errCh:
   210  		return "", err
   211  	}
   212  }