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 }