mirror of
https://github.com/status-im/status-console-client.git
synced 2025-02-23 08:08:27 +00:00
This method will ensure that we will re-dial mailserver if connection was lost without waiting for 30s (devp2p timeout) and we will catch if connection already exists (waiting for event doesn't existing connections)
646 lines
16 KiB
Go
646 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"encoding/hex"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
ossignal "os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
gethnode "github.com/ethereum/go-ethereum/node"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
|
|
"github.com/status-im/status-go/logutils"
|
|
"github.com/status-im/status-go/node"
|
|
"github.com/status-im/status-go/params"
|
|
"github.com/status-im/status-go/services/shhext/chat"
|
|
"github.com/status-im/status-go/signal"
|
|
|
|
"github.com/jroimartin/gocui"
|
|
"github.com/peterbourgon/ff"
|
|
"github.com/pkg/errors"
|
|
|
|
datasyncnode "github.com/status-im/mvds/node"
|
|
"github.com/status-im/mvds/state"
|
|
"github.com/status-im/mvds/store"
|
|
|
|
"github.com/status-im/status-console-client/protocol/adapter"
|
|
"github.com/status-im/status-console-client/protocol/client"
|
|
"github.com/status-im/status-console-client/protocol/datasync"
|
|
dspeer "github.com/status-im/status-console-client/protocol/datasync/peer"
|
|
"github.com/status-im/status-console-client/protocol/gethservice"
|
|
"github.com/status-im/status-console-client/protocol/transport"
|
|
"github.com/status-im/status-console-client/protocol/v1"
|
|
)
|
|
|
|
var g *gocui.Gui
|
|
|
|
var (
|
|
fs = flag.NewFlagSet("status-term-client", flag.ExitOnError)
|
|
logLevel = fs.String("log-level", "INFO", "log level")
|
|
|
|
keyHex = fs.String("keyhex", "", "pass a private key in hex")
|
|
noUI = fs.Bool("no-ui", false, "disable UI")
|
|
|
|
// flags acting like commands
|
|
createKeyPair = fs.Bool("create-key-pair", false, "creates and prints a key pair instead of running")
|
|
addContact = fs.String("add-contact", "", "add contact using format: type,name[,public-key] where type can be 'private' or 'public' and 'public-key' is required for 'private' type")
|
|
|
|
// flags for in-proc node
|
|
dataDir = fs.String("data-dir", filepath.Join(os.TempDir(), "status-term-client"), "data directory for Ethereum node")
|
|
noNamespace = fs.Bool("no-namespace", false, "disable data dir namespacing with public key")
|
|
fleet = fs.String("fleet", params.FleetBeta, fmt.Sprintf("Status nodes cluster to connect to: %s", []string{params.FleetBeta, params.FleetStaging}))
|
|
configFile = fs.String("node-config", "", "a JSON file with node config")
|
|
pfsEnabled = fs.Bool("pfs", false, "enable PFS")
|
|
dataSyncEnabled = fs.Bool("ds", false, "enable data sync")
|
|
|
|
// flags for external node
|
|
providerURI = fs.String("provider", "", "an URI pointing at a provider")
|
|
)
|
|
|
|
func main() {
|
|
if err := ff.Parse(fs, os.Args[1:]); err != nil {
|
|
exitErr(errors.Wrap(err, "failed to parse flags"))
|
|
}
|
|
|
|
if *createKeyPair {
|
|
key, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
fmt.Printf("Your private key: %#x\n", crypto.FromECDSA(key))
|
|
os.Exit(0)
|
|
}
|
|
|
|
var privateKey *ecdsa.PrivateKey
|
|
|
|
if *keyHex != "" {
|
|
k, err := crypto.HexToECDSA(strings.TrimPrefix(*keyHex, "0x"))
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
privateKey = k
|
|
} else {
|
|
k, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
privateKey = k
|
|
fmt.Printf("Starting with a new private key: %#x\n", crypto.FromECDSA(privateKey))
|
|
}
|
|
|
|
// Prefix data directory with a public key.
|
|
// This is required because it's not possible
|
|
// or adviced to share data between different
|
|
// key pairs.
|
|
if !*noNamespace {
|
|
*dataDir = filepath.Join(
|
|
*dataDir,
|
|
hex.EncodeToString(crypto.FromECDSAPub(&privateKey.PublicKey)[:20]),
|
|
)
|
|
}
|
|
|
|
err := os.MkdirAll(*dataDir, 0755)
|
|
if err != nil {
|
|
exitErr(err)
|
|
} else {
|
|
fmt.Printf("Starting in %s\n", *dataDir)
|
|
}
|
|
|
|
// Setup logging by splitting it into a client.log
|
|
// with status-console-client logs and status.log
|
|
// with Status Node logs.
|
|
clientLogPath := filepath.Join(*dataDir, "client.log")
|
|
clientLogFile, err := os.OpenFile(clientLogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
log.SetOutput(clientLogFile)
|
|
|
|
nodeLogPath := filepath.Join(*dataDir, "status.log")
|
|
err = logutils.OverrideRootLog(true, *logLevel, logutils.FileOptions{Filename: nodeLogPath}, false)
|
|
if err != nil {
|
|
exitErr(fmt.Errorf("failed to override root log: %v", err))
|
|
}
|
|
|
|
// Create a database.
|
|
// TODO(adam): currently, we use an address as a db encryption key.
|
|
// It should be configurable.
|
|
dbKey := crypto.PubkeyToAddress(privateKey.PublicKey).String()
|
|
dbPath := filepath.Join(*dataDir, "db.sql")
|
|
db, err := client.InitializeDB(dbPath, dbKey)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Log the current contact info in two places for easy retrieval.
|
|
fmt.Printf("Contact address: %#x\n", crypto.FromECDSAPub(&privateKey.PublicKey))
|
|
log.Printf("contact address: %#x", crypto.FromECDSAPub(&privateKey.PublicKey))
|
|
|
|
// Manage initial contacts.
|
|
if contacts, err := db.Contacts(); len(contacts) == 0 || err != nil {
|
|
debugContacts := []client.Contact{
|
|
{Name: "status", Type: client.ContactPublicRoom, Topic: "status"},
|
|
{Name: "status-core", Type: client.ContactPublicRoom, Topic: "status-core"},
|
|
}
|
|
uniqueContacts := []client.Contact{}
|
|
for _, c := range debugContacts {
|
|
exist, err := db.ContactExist(c)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
if !exist {
|
|
uniqueContacts = append(uniqueContacts, c)
|
|
}
|
|
}
|
|
if len(uniqueContacts) != 0 {
|
|
if err := db.SaveContacts(uniqueContacts); err != nil {
|
|
exitErr(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle add contact.
|
|
if *addContact != "" {
|
|
options := strings.Split(*addContact, ",")
|
|
|
|
var c client.Contact
|
|
|
|
if len(options) == 2 && options[0] == "public" {
|
|
c = client.Contact{
|
|
Name: options[1],
|
|
Type: client.ContactPublicRoom,
|
|
Topic: options[1],
|
|
}
|
|
} else if len(options) == 3 && options[0] == "private" {
|
|
c, err = client.CreateContactPrivate(options[1], options[2], client.ContactAdded)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
} else {
|
|
exitErr(errors.Errorf("invalid -add-contact value"))
|
|
}
|
|
|
|
exists, err := db.ContactExist(c)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
if !exists {
|
|
if err := db.SaveContacts([]client.Contact{c}); err != nil {
|
|
exitErr(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// initialize protocol
|
|
var (
|
|
messenger *client.Messenger
|
|
)
|
|
|
|
if *providerURI != "" {
|
|
messenger, err = createMessengerWithURI(*providerURI, privateKey, db)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
} else {
|
|
messenger, err = createMessengerInProc(privateKey, db)
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
}
|
|
|
|
// run in a goroutine to show the UI faster
|
|
go func() {
|
|
if err := messenger.Start(); err != nil {
|
|
exitErr(err)
|
|
}
|
|
}()
|
|
|
|
done := make(chan bool, 1)
|
|
sigs := make(chan os.Signal, 1)
|
|
|
|
ossignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
sig := <-sigs
|
|
log.Printf("received signal: %v", sig)
|
|
done <- true
|
|
}()
|
|
|
|
log.Printf("starting UI...")
|
|
|
|
if !*noUI {
|
|
go func() {
|
|
<-done
|
|
exitErr(errors.New("exit with signal"))
|
|
}()
|
|
|
|
if err := setupGUI(privateKey, messenger); err != nil {
|
|
exitErr(err)
|
|
}
|
|
|
|
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
|
exitErr(err)
|
|
}
|
|
|
|
g.Close()
|
|
} else {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func exitErr(err error) {
|
|
if g != nil {
|
|
g.Close()
|
|
}
|
|
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
type keysGetter struct {
|
|
privateKey *ecdsa.PrivateKey
|
|
}
|
|
|
|
func (k keysGetter) PrivateKey() (*ecdsa.PrivateKey, error) {
|
|
return k.privateKey, nil
|
|
}
|
|
|
|
func createMessengerWithURI(uri string, pk *ecdsa.PrivateKey, db client.Database) (*client.Messenger, error) {
|
|
_, err := rpc.Dial(*providerURI)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to dial")
|
|
}
|
|
|
|
// TODO: provide Mail Servers in a different way.
|
|
_, err = generateStatusNodeConfig(*dataDir, *fleet, *configFile)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to generate node config")
|
|
}
|
|
|
|
// TODO
|
|
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func createMessengerInProc(pk *ecdsa.PrivateKey, db client.Database) (*client.Messenger, error) {
|
|
// collect mail server request signals
|
|
signalsForwarder := newSignalForwarder()
|
|
go signalsForwarder.Start()
|
|
|
|
// setup signals handler
|
|
signal.SetDefaultNodeNotificationHandler(
|
|
filterMailTypesHandler(signalsForwarder.in),
|
|
)
|
|
|
|
nodeConfig, err := generateStatusNodeConfig(*dataDir, *fleet, *configFile)
|
|
if err != nil {
|
|
exitErr(errors.Wrap(err, "failed to generate node config"))
|
|
}
|
|
|
|
statusNode := node.New()
|
|
|
|
protocolGethService := gethservice.New(
|
|
statusNode,
|
|
&keysGetter{privateKey: pk},
|
|
)
|
|
|
|
services := []gethnode.ServiceConstructor{
|
|
func(ctx *gethnode.ServiceContext) (gethnode.Service, error) {
|
|
return protocolGethService, nil
|
|
},
|
|
}
|
|
|
|
if err := statusNode.Start(nodeConfig, services...); err != nil {
|
|
return nil, errors.Wrap(err, "failed to start node")
|
|
}
|
|
|
|
shhService, err := statusNode.WhisperService()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get Whisper service")
|
|
}
|
|
|
|
shhExtService, err := statusNode.ShhExtService()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get shhext service")
|
|
}
|
|
|
|
var (
|
|
protocolAdapter protocol.Protocol
|
|
transp = transport.NewWhisperServiceTransport(
|
|
&server{node: statusNode},
|
|
nodeConfig.ClusterConfig.TrustedMailServers,
|
|
shhService,
|
|
shhExtService,
|
|
pk,
|
|
)
|
|
)
|
|
|
|
if *dataSyncEnabled {
|
|
dataSyncTransport := datasync.NewDataSyncNodeTransport(transp)
|
|
dataSyncStore := store.NewDummyStore()
|
|
dataSyncNode := datasyncnode.NewNode(
|
|
&dataSyncStore,
|
|
dataSyncTransport,
|
|
state.NewSyncState(), // @todo sqlite syncstate
|
|
datasync.CalculateSendTime,
|
|
0,
|
|
dspeer.PublicKeyToPeerID(pk.PublicKey),
|
|
datasyncnode.BATCH,
|
|
)
|
|
|
|
dataSyncNode.Start()
|
|
|
|
protocolAdapter = adapter.NewDataSyncWhisperAdapter(dataSyncNode, transp, dataSyncTransport)
|
|
} else {
|
|
var pfs *chat.ProtocolService
|
|
|
|
// TODO: should be removed from StatusNode
|
|
if *pfsEnabled {
|
|
databasesDir := filepath.Join(*dataDir, "databases")
|
|
|
|
if err := os.MkdirAll(databasesDir, 0755); err != nil {
|
|
exitErr(errors.Wrap(err, "failed to create databases dir"))
|
|
}
|
|
|
|
pfs, err = initPFS(databasesDir)
|
|
if err != nil {
|
|
exitErr(errors.Wrap(err, "initialize PFS"))
|
|
}
|
|
|
|
log.Printf("PFS has been initialized")
|
|
}
|
|
|
|
protocolAdapter = adapter.NewProtocolWhisperAdapter(transp, pfs)
|
|
}
|
|
|
|
messenger := client.NewMessenger(pk, protocolAdapter, db)
|
|
|
|
protocolGethService.SetProtocol(protocolAdapter)
|
|
protocolGethService.SetMessenger(messenger)
|
|
|
|
return messenger, nil
|
|
}
|
|
|
|
func setupGUI(privateKey *ecdsa.PrivateKey, messenger *client.Messenger) error {
|
|
var err error
|
|
|
|
// global
|
|
g, err = gocui.NewGui(gocui.Output256)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// prepare views
|
|
vm := NewViewManager(nil, g)
|
|
|
|
notifications := NewNotificationViewController(&ViewController{vm, g, ViewNotification})
|
|
|
|
chat := NewChatViewController(
|
|
&ViewController{vm, g, ViewChat},
|
|
privateKey,
|
|
messenger,
|
|
func(err error) {
|
|
_ = notifications.Error("Chat error", fmt.Sprintf("%v", err))
|
|
},
|
|
)
|
|
|
|
contacts := NewContactsViewController(&ViewController{vm, g, ViewContacts}, messenger)
|
|
if err := contacts.LoadAndRefresh(); err != nil {
|
|
return err
|
|
}
|
|
|
|
inputMultiplexer := NewInputMultiplexer()
|
|
inputMultiplexer.AddHandler(DefaultMultiplexerPrefix, func(b []byte) error {
|
|
log.Printf("default multiplexer handler")
|
|
return chat.Send(b)
|
|
})
|
|
inputMultiplexer.AddHandler("/contact", ContactCmdFactory(contacts))
|
|
inputMultiplexer.AddHandler("/request", RequestCmdFactory(chat))
|
|
|
|
views := []*View{
|
|
&View{
|
|
Name: ViewContacts,
|
|
Enabled: true,
|
|
Cursor: true,
|
|
Highlight: true,
|
|
SelBgColor: gocui.ColorGreen,
|
|
SelFgColor: gocui.ColorBlack,
|
|
TopLeft: func(maxX, maxY int) (int, int) { return 0, 0 },
|
|
BottomRight: func(maxX, maxY int) (int, int) {
|
|
return int(math.Floor(float64(maxX) * 0.2)), maxY - 4
|
|
},
|
|
Keybindings: []Binding{
|
|
Binding{
|
|
Key: gocui.KeyArrowDown,
|
|
Mod: gocui.ModNone,
|
|
Handler: CursorDownHandler,
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyArrowUp,
|
|
Mod: gocui.ModNone,
|
|
Handler: CursorUpHandler,
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyEnter,
|
|
Mod: gocui.ModNone,
|
|
Handler: GetLineHandler(func(idx int, _ string) error {
|
|
contact, ok := contacts.ContactByIdx(idx)
|
|
if !ok {
|
|
return errors.New("contact could not be found")
|
|
}
|
|
|
|
// We need to call Select asynchronously,
|
|
// otherwise the main thread is blocked
|
|
// and nothing is rendered.
|
|
go func() {
|
|
if err := chat.Select(contact); err != nil {
|
|
log.Printf("[GetLineHandler] error selecting a chat: %v", err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
&View{
|
|
Name: ViewChat,
|
|
Enabled: true,
|
|
Cursor: true,
|
|
Autoscroll: false,
|
|
Highlight: true,
|
|
Wrap: true,
|
|
SelBgColor: gocui.ColorGreen,
|
|
SelFgColor: gocui.ColorBlack,
|
|
TopLeft: func(maxX, maxY int) (int, int) {
|
|
return int(math.Ceil(float64(maxX) * 0.2)), 0
|
|
},
|
|
BottomRight: func(maxX, maxY int) (int, int) {
|
|
return maxX - 1, maxY - 4
|
|
},
|
|
Keybindings: []Binding{
|
|
Binding{
|
|
Key: gocui.KeyArrowDown,
|
|
Mod: gocui.ModNone,
|
|
Handler: CursorDownHandler,
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyArrowUp,
|
|
Mod: gocui.ModNone,
|
|
Handler: CursorUpHandler,
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyHome,
|
|
Mod: gocui.ModNone,
|
|
Handler: func(g *gocui.Gui, v *gocui.View) error {
|
|
params, err := chat.RequestOptions(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := notifications.Debug("Messages request", fmt.Sprintf("%v", params)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// RequestMessages needs to be called asynchronously,
|
|
// otherwise the main thread is blocked
|
|
// and nothing is rendered.
|
|
go func() {
|
|
if err := chat.RequestMessages(params); err != nil {
|
|
_ = notifications.Error("Request failed", fmt.Sprintf("%v", err))
|
|
}
|
|
}()
|
|
|
|
return HomeHandler(g, v)
|
|
},
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyEnd,
|
|
Mod: gocui.ModNone,
|
|
Handler: EndHandler,
|
|
},
|
|
},
|
|
},
|
|
&View{
|
|
Name: ViewInput,
|
|
Title: fmt.Sprintf(
|
|
"%s (as %#x)",
|
|
ViewInput,
|
|
crypto.FromECDSAPub(&privateKey.PublicKey),
|
|
),
|
|
Enabled: true,
|
|
Editable: true,
|
|
Cursor: true,
|
|
Highlight: true,
|
|
TopLeft: func(maxX, maxY int) (int, int) {
|
|
return 0, maxY - 3
|
|
},
|
|
BottomRight: func(maxX, maxY int) (int, int) {
|
|
return maxX - 1, maxY - 1
|
|
},
|
|
Keybindings: []Binding{
|
|
Binding{
|
|
Key: gocui.KeyEnter,
|
|
Mod: gocui.ModNone,
|
|
Handler: inputMultiplexer.BindingHandler,
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyEnter,
|
|
Mod: gocui.ModAlt,
|
|
Handler: MoveToNewLineHandler,
|
|
},
|
|
},
|
|
},
|
|
&View{
|
|
Name: ViewNotification,
|
|
Enabled: false,
|
|
Editable: false,
|
|
Cursor: false,
|
|
Highlight: true,
|
|
TopLeft: func(maxX, maxY int) (int, int) {
|
|
return maxX/2 - 50, maxY / 2
|
|
},
|
|
BottomRight: func(maxX, maxY int) (int, int) {
|
|
return maxX/2 + 50, maxY/2 + 2
|
|
},
|
|
Keybindings: []Binding{
|
|
Binding{
|
|
Key: gocui.KeyEnter,
|
|
Mod: gocui.ModNone,
|
|
Handler: func(g *gocui.Gui, v *gocui.View) error {
|
|
log.Printf("Notification Enter binding")
|
|
|
|
if err := vm.DisableView(ViewNotification); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := vm.DeleteView(ViewNotification); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
bindings := []Binding{
|
|
Binding{
|
|
Key: gocui.KeyCtrlC,
|
|
Mod: gocui.ModNone,
|
|
Handler: QuitHandler,
|
|
},
|
|
Binding{
|
|
Key: gocui.KeyTab,
|
|
Mod: gocui.ModNone,
|
|
Handler: NextViewHandler(vm),
|
|
},
|
|
}
|
|
|
|
if err := vm.SetViews(views); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := vm.SetGlobalKeybindings(bindings); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func initPFS(baseDir string) (*chat.ProtocolService, error) {
|
|
const (
|
|
// TODO: manage these values properly
|
|
dbFileName = "pfs_v1.db"
|
|
sqlSecretKey = "enc-key-abc"
|
|
instalationID = "instalation-1"
|
|
)
|
|
|
|
dbPath := filepath.Join(baseDir, dbFileName)
|
|
persistence, err := chat.NewSQLLitePersistence(dbPath, sqlSecretKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addBundlesHandler := func(addedBundles []chat.IdentityAndIDPair) {
|
|
log.Printf("added bundles: %v", addedBundles)
|
|
}
|
|
|
|
return chat.NewProtocolService(
|
|
chat.NewEncryptionService(
|
|
persistence,
|
|
chat.DefaultEncryptionServiceConfig(instalationID),
|
|
),
|
|
addBundlesHandler,
|
|
), nil
|
|
}
|