Send direct message using CLI (#4913)

* feat: send dm with cli

* fix: send contact request works

* feat: accept contact request

* feat: send dm

* fix: log

* feat: more dm

* fix: lint

* refactor: fix comments

* fix: more refactor

* fix: refoctor more

* fix: more refacotr

* fix: refactor dm

* fix: context from cli

* fix: add light mode flag

* fix: remove sleep when start node

* fix: better log

* fix: better logger

* fix: lint

* fix: comments

* fix: const flags

* fix: named logger

* feat: inteactive mode

* fix: send message in loop

* fix: better context manage

* feat: cli serve

* fix: readme

* extract to multi files

* fix: status cli rpc

* feat: create and login account

* fix: missing messagess

* fix: missing messages because data sync is not started.

* feat: start http service

* fix: lint

* fix: more build instructions.

* fix: review comments
This commit is contained in:
kaichao 2024-03-19 08:31:35 +08:00 committed by GitHub
parent 17c5ab414b
commit f0d6a4f64f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 644 additions and 5 deletions

6
.gitignore vendored
View File

@ -92,3 +92,9 @@ test_*.log
vendor/github.com/waku-org/go-zerokit-rln-x86_64/
vendor/github.com/waku-org/go-zerokit-rln-apple/
vendor/github.com/waku-org/go-zerokit-rln-arm/
# status-cli logs
alice.log
bob.log
test-alice/
test-bob/

View File

@ -111,6 +111,9 @@ statusd-prune: build/bin/statusd-prune
spiff-workflow: ##@build Build node for SpiffWorkflow BPMN software
spiff-workflow: build/bin/spiff-workflow
status-cli: ##@build Build status-cli to send messages
status-cli: build/bin/status-cli
statusd-prune-docker-image: ##@statusd-prune Build statusd-prune docker image
@echo "Building docker image for ststusd-prune..."
docker build --file _assets/build/Dockerfile-prune . \

View File

@ -186,7 +186,7 @@ func buildWalletConfig(request *requests.WalletSecretsConfig) params.WalletConfi
return walletConfig
}
func defaultNodeConfig(installationID string, request *requests.CreateAccount) (*params.NodeConfig, error) {
func defaultNodeConfig(installationID string, request *requests.CreateAccount, opts ...params.Option) (*params.NodeConfig, error) {
// Set mainnet
nodeConfig := &params.NodeConfig{}
nodeConfig.NetworkID = request.NetworkID
@ -272,6 +272,12 @@ func defaultNodeConfig(installationID string, request *requests.CreateAccount) (
nodeConfig.Networks = BuildDefaultNetworks(request)
for _, opt := range opts {
if err := opt(nodeConfig); err != nil {
return nil, err
}
}
return nodeConfig, nil
}

View File

@ -1298,7 +1298,7 @@ func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error)
return info.KeyUID, nil
}
func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, request *requests.CreateAccount) (*multiaccounts.Account, error) {
func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, error) {
keystoreDir := keystoreRelativePath
b.UpdateRootDataDir(request.BackupDisabledDataDir)
@ -1382,7 +1382,7 @@ func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizati
//settings.MnemonicWasNotShown = true
}
nodeConfig, err := defaultNodeConfig(settings.InstallationID, request)
nodeConfig, err := defaultNodeConfig(settings.InstallationID, request, opts...)
if err != nil {
return nil, err
}
@ -1430,12 +1430,12 @@ func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizati
return &account, nil
}
func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccount) (*multiaccounts.Account, error) {
func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, error) {
if err := request.Validate(); err != nil {
return nil, err
}
return b.generateOrImportAccount("", 1, request)
return b.generateOrImportAccount("", 1, request, opts...)
}
func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error {

3
cmd/status-cli/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
log
*.log
status-cli

70
cmd/status-cli/README.md Normal file
View File

@ -0,0 +1,70 @@
# Status CLI
The Status CLI is a command line interface for interacting with the Status messenging process. It is a tool for developers and QAs to test the communication workflow without running Status desktop and mobile app.
## Features
- Create a new account
- Send and receive contact request
- DM between contacts
## Build
Go to `cmd/status-cli` directory and build the binary
```bash
go build
```
You can also run `make status-cli` in the root directory to build the binary.
## Run
### Run `serve` command:
```bash
# run alice's server
./status-cli serve
# run Bob's server in another terminal with the logged pubkey of Alice
./status-cli serve -n bob -p 8565 -a <alice-pubkey>
```
You can send direct messages through terminal or JSON RPC.
JSON RPC examples:
```bash
# get waku info
curl -XPOST http://127.0.0.1:8545 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"waku_info","params":[],"id":1}'
# send contact request from bob to alice (use -a flag will automatacally send contact request when starting)
curl -XPOST http://127.0.0.1:8565 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"wakuext_sendContactRequest","params":[{"id": "0x0436470da23039f10c1588bc6b9fcbd4b815bf9fae4dc09c0fb05a7eaaf1670b5dbdbc757630d54bf2f8be45a796304dc42506c3f4172f499f610a9ed85d9b0d4c", "message": "hello"}],"id":1}'
# send dm from bob to alice
curl -XPOST http://127.0.0.1:8565 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"wakuext_sendOneToOneMessage","params":[{"id": "0x0436470da23039f10c1588bc6b9fcbd4b815bf9fae4dc09c0fb05a7eaaf1670b5dbdbc757630d54bf2f8be45a796304dc42506c3f4172f499f610a9ed85d9b0d4c", "message": "how are you"}],"id":1}'
# send dm from alice to bob
curl -XPOST http://127.0.0.1:8545 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"wakuext_sendOneToOneMessage","params":[{"id": "0x042c0ce856c41ad6d3f651a84c83f646cdafdf3a26a3d69bce3a6ccf59b23b5a366c12162045d5066abad7912741a6e6c6e8e11e7826c4c850a1de7a2bae24a79c", "message": "Im fine, and you?"}],"id":1}'
```
### Run `dm` command:
```bash
# simulate DM between two accounts
./status-cli dm
# simulate DM in a interactive way
./status-cli dm -i
# simulate DM with 3 messages
./status-cli dm -c 3
# run in light mode
./status-cli dm --light
```
You can run the commands with `--light` to work as a light client.
Logs are recorded in file `*.log` and terminal.

104
cmd/status-cli/main.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"os"
"time"
"go.uber.org/zap"
"github.com/status-im/status-go/api"
"github.com/status-im/status-go/protocol"
"github.com/urfave/cli/v2"
)
const LightFlag = "light"
const InteractiveFlag = "interactive"
const CountFlag = "count"
const NameFlag = "name"
const AddFlag = "add"
const PortFlag = "port"
const RetrieveInterval = 300 * time.Millisecond
const SendInterval = 1 * time.Second
const WaitingInterval = 5 * time.Second
var CommonFlags = []cli.Flag{
&cli.BoolFlag{
Name: LightFlag,
Aliases: []string{"l"},
Usage: "Enable light mode",
},
}
var DmFlags = append([]cli.Flag{
&cli.BoolFlag{
Name: InteractiveFlag,
Aliases: []string{"i"},
Usage: "Use interactive mode",
},
&cli.IntFlag{
Name: CountFlag,
Aliases: []string{"c"},
Value: 1,
Usage: "How many messages to sent from each user",
},
}, CommonFlags...)
var ServeFlags = append([]cli.Flag{
&cli.StringFlag{
Name: NameFlag,
Aliases: []string{"n"},
Value: "Alice",
Usage: "Name of the user",
},
&cli.StringFlag{
Name: AddFlag,
Aliases: []string{"a"},
Usage: "Add a friend with the public key",
},
&cli.IntFlag{
Name: PortFlag,
Aliases: []string{"p"},
Value: 8545,
Usage: "HTTP Server port to listen on",
},
}, CommonFlags...)
var logger *zap.SugaredLogger
type StatusCLI struct {
name string
messenger *protocol.Messenger
backend *api.GethStatusBackend
logger *zap.SugaredLogger
}
func main() {
app := &cli.App{
Commands: []*cli.Command{
{
Name: "dm",
Aliases: []string{"d"},
Usage: "Send direct message",
Flags: DmFlags,
Action: func(cCtx *cli.Context) error {
return simulate(cCtx)
},
},
{
Name: "serve",
Aliases: []string{"s"},
Usage: "Start a server to send and receive messages",
Flags: ServeFlags,
Action: func(cCtx *cli.Context) error {
return serve(cCtx)
},
},
},
}
if err := app.Run(os.Args); err != nil {
logger.Fatal(err)
}
}

144
cmd/status-cli/message.go Normal file
View File

@ -0,0 +1,144 @@
package main
import (
"bufio"
"context"
"os"
"strings"
"sync"
"time"
"github.com/status-im/status-go/eth-node/types"
"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 sendContactRequest(cCtx *cli.Context, from *StatusCLI, toID string) error {
from.logger.Info("send contact request, contact public key: ", toID)
request := &requests.SendContactRequest{
ID: toID,
Message: "Hello!",
}
resp, err := from.messenger.SendContactRequest(cCtx.Context, request)
from.logger.Info("function SendContactRequest response.messages: ", resp.Messages())
if err != nil {
return err
}
return nil
}
func sendContactRequestAcceptance(cCtx *cli.Context, from *StatusCLI, msgID string) error {
from.logger.Info("accept contact request, message ID: ", msgID)
resp, err := from.messenger.AcceptContactRequest(cCtx.Context, &requests.AcceptContactRequest{ID: types.Hex2Bytes(msgID)})
if err != nil {
return err
}
from.logger.Info("function AcceptContactRequest response: ", resp.Messages())
return nil
}
func sendDirectMessage(ctx context.Context, from *StatusCLI, text string) error {
if len(from.messenger.MutualContacts()) == 0 {
return nil
}
chat := from.messenger.Chat(from.messenger.MutualContacts()[0].ID)
from.logger.Info("send message to contact: ", chat.ID)
clock, timestamp := chat.NextClockAndTimestamp(from.messenger.GetTransport())
inputMessage := common.NewMessage()
inputMessage.ChatId = chat.ID
inputMessage.LocalChatID = chat.ID
inputMessage.Clock = clock
inputMessage.Timestamp = timestamp
inputMessage.MessageType = protobuf.MessageType_ONE_TO_ONE
inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
inputMessage.Text = text
_, err := from.messenger.SendChatMessage(ctx, inputMessage)
if err != nil {
return err
}
return nil
}
func retrieveMessagesLoop(ctx context.Context, cli *StatusCLI, tick time.Duration, msgCh chan string, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(tick)
defer ticker.Stop()
cli.logger.Info("retrieve messages...")
for {
select {
case <-ticker.C:
response, err := cli.messenger.RetrieveAll()
if err != nil {
cli.logger.Error("failed to retrieve raw messages", "err", err)
continue
}
if response != nil && len(response.Messages()) != 0 {
for _, message := range response.Messages() {
cli.logger.Info("receive message: ", message.Text)
if message.ContentType == protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT {
msgCh <- message.ID
}
}
}
case <-ctx.Done():
return
}
}
}
func sendMessageLoop(ctx context.Context, cli *StatusCLI, tick time.Duration, wg *sync.WaitGroup, sem chan struct{}, cancel context.CancelFunc) {
defer wg.Done()
ticker := time.NewTicker(tick)
defer ticker.Stop()
reader := bufio.NewReader(os.Stdin)
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
}
message = strings.TrimSpace(message)
if message == "quit" || message == "q" || strings.Contains(message, "\x03") {
cancel()
<-sem
return
}
if message == "" {
<-sem
continue
}
err = sendDirectMessage(ctx, cli, message)
time.Sleep(WaitingInterval)
<-sem
if err != nil {
cli.logger.Error("failed to send direct message: ", err)
continue
}
case <-ctx.Done():
return
}
}
}

79
cmd/status-cli/serve.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"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)
}
logger = rawLogger.Sugar()
logger.Info("Running serve command, flags passed:")
for _, flag := range ServeFlags {
logger.Infof("-%s %v", flag.Names()[0], cCtx.Value(flag.Names()[0]))
}
name := cCtx.String(NameFlag)
port := cCtx.Int(PortFlag)
messenger, err := startMessenger(cCtx, name, port)
if err != nil {
return err
}
defer stopMessenger(messenger)
// Retrieve for messages
var wg sync.WaitGroup
msgCh := make(chan string)
wg.Add(1)
go retrieveMessagesLoop(ctx, messenger, RetrieveInterval, msgCh, &wg)
// Send contact request from Alice to Bob, bob accept the request
dest := cCtx.String(AddFlag)
if dest != "" {
err := sendContactRequest(cCtx, messenger, dest)
if err != nil {
return err
}
}
go func() {
msgID := <-msgCh
err = sendContactRequestAcceptance(cCtx, messenger, msgID)
if err != nil {
logger.Error(err)
return
}
}()
// Send message if mutual contact exists
sem := make(chan struct{}, 1)
wg.Add(1)
go sendMessageLoop(ctx, messenger, SendInterval, &wg, sem, cancel)
wg.Wait()
logger.Info("Exiting")
return nil
}

106
cmd/status-cli/simulate.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
)
func simulate(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)
}
logger = rawLogger.Sugar()
logger.Info("Running dm command, flags passed:")
for _, flag := range DmFlags {
logger.Infof("-%s %v", flag.Names()[0], cCtx.Value(flag.Names()[0]))
}
// Start Alice and Bob's messengers
alice, err := startMessenger(cCtx, "Alice", 0)
if err != nil {
return err
}
defer stopMessenger(alice)
bob, err := startMessenger(cCtx, "Bob", 0)
if err != nil {
return err
}
defer stopMessenger(bob)
// Retrieve for messages
msgCh := make(chan string)
var wg sync.WaitGroup
wg.Add(1)
go retrieveMessagesLoop(ctx, alice, RetrieveInterval, nil, &wg)
wg.Add(1)
go retrieveMessagesLoop(ctx, bob, RetrieveInterval, msgCh, &wg)
// Send contact request from Alice to Bob, bob accept the request
time.Sleep(WaitingInterval)
destID := types.EncodeHex(crypto.FromECDSAPub(bob.messenger.IdentityPublicKey()))
err = sendContactRequest(cCtx, alice, destID)
if err != nil {
return err
}
msgID := <-msgCh
err = sendContactRequestAcceptance(cCtx, bob, msgID)
if err != nil {
return err
}
// Send DM between alice to bob
interactive := cCtx.Bool(InteractiveFlag)
if interactive {
sem := make(chan struct{}, 1)
wg.Add(1)
go sendMessageLoop(ctx, alice, SendInterval, &wg, sem, cancel)
wg.Add(1)
go sendMessageLoop(ctx, bob, SendInterval, &wg, sem, cancel)
} else {
time.Sleep(WaitingInterval)
for i := 0; i < cCtx.Int(CountFlag); i++ {
err = sendDirectMessage(ctx, alice, "hello bob :)")
if err != nil {
return err
}
time.Sleep(WaitingInterval)
err = sendDirectMessage(ctx, bob, "hello Alice ~")
if err != nil {
return err
}
time.Sleep(WaitingInterval)
}
cancel()
}
wg.Wait()
logger.Info("Exiting")
return nil
}

111
cmd/status-cli/util.go Normal file
View File

@ -0,0 +1,111 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/status-im/status-go/api"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/params"
"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"
)
func setupLogger(file string) *zap.Logger {
logFile := fmt.Sprintf("%s.log", strings.ToLower(file))
logSettings := logutils.LogSettings{
Enabled: true,
MobileSystem: false,
Level: "DEBUG",
File: logFile,
MaxSize: 100,
MaxBackups: 3,
CompressRotated: true,
}
if err := logutils.OverrideRootLogWithConfig(logSettings, false); err != nil {
logger.Fatalf("Error initializing logger: %v", err)
}
newLogger := logutils.ZapLogger()
return newLogger
}
func withHTTP(port int) params.Option {
return func(c *params.NodeConfig) error {
c.APIModules = "waku,wakuext,wakuv2,permissions,eth"
c.HTTPEnabled = true
c.HTTPHost = "127.0.0.1"
c.HTTPPort = port
return nil
}
}
func startMessenger(cCtx *cli.Context, name string, port int) (*StatusCLI, error) {
namedLogger := logger.Named(name)
namedLogger.Info("starting messager")
_ = setupLogger(name)
path := fmt.Sprintf("./test-%s", strings.ToLower(name))
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return nil, err
}
backend := api.NewGethStatusBackend()
createAccountRequest := &requests.CreateAccount{
DisplayName: "some-display-name",
CustomizationColor: "#ffffff",
Emoji: "some",
Password: "some-password",
BackupDisabledDataDir: fmt.Sprintf("./test-%s", strings.ToLower(name)),
NetworkID: 1,
LogFilePath: "log",
}
opts := []params.Option{withHTTP(port)}
_, err = backend.CreateAccountAndLogin(createAccountRequest, opts...)
if err != nil {
return nil, err
}
wakuService := backend.StatusNode().WakuV2ExtService()
if wakuService == nil {
return nil, errors.New("waku service is not available")
}
wakuAPI := wakuv2ext.NewPublicAPI(wakuService)
messenger := wakuAPI.Messenger()
_, err = wakuAPI.StartMessenger()
if err != nil {
return nil, err
}
namedLogger.Info("messenger started, public key: ", messenger.IdentityPublicKeyString())
time.Sleep(WaitingInterval)
data := StatusCLI{
name: name,
messenger: messenger,
backend: backend,
logger: namedLogger,
}
return &data, nil
}
func stopMessenger(cli *StatusCLI) {
err := cli.backend.StopNode()
if err != nil {
logger.Error(err)
}
}

View File

@ -3489,6 +3489,10 @@ func (m *Messenger) GetStats() types.StatsSummary {
return m.transport.GetStats()
}
func (m *Messenger) GetTransport() *transport.Transport {
return m.transport
}
type CurrentMessageState struct {
// Message is the protobuf message received
Message *protobuf.ChatMessage

View File

@ -470,6 +470,9 @@ func rowsToTransactions(rows *sql.Rows) (transactions []*PendingTransaction, err
}
func (tm *PendingTxTracker) GetAllPending() ([]*PendingTransaction, error) {
if tm.db == nil {
return nil, errors.New("database is not initialized")
}
rows, err := tm.db.Query(selectFromPending+"WHERE status = ?", Pending)
if err != nil {
return nil, err