2019-01-08 21:02:11 +01:00
package main
import (
"crypto/ecdsa"
2019-05-28 10:49:50 +02:00
"encoding/hex"
2019-01-08 21:02:11 +01:00
"flag"
"fmt"
"log"
"math"
"os"
2019-04-30 10:24:33 +02:00
ossignal "os/signal"
2019-01-08 21:02:11 +01:00
"path/filepath"
"strings"
2019-04-30 10:24:33 +02:00
"syscall"
2019-01-08 21:02:11 +01:00
"github.com/ethereum/go-ethereum/crypto"
2019-04-12 16:53:26 +02:00
gethnode "github.com/ethereum/go-ethereum/node"
2019-03-11 18:49:18 +01:00
"github.com/ethereum/go-ethereum/rpc"
2019-01-08 21:02:11 +01:00
"github.com/jroimartin/gocui"
"github.com/peterbourgon/ff"
2019-03-11 18:49:18 +01:00
"github.com/pkg/errors"
2019-03-18 09:53:39 +01:00
"github.com/status-im/status-console-client/protocol/adapters"
2019-03-11 18:49:18 +01:00
"github.com/status-im/status-console-client/protocol/client"
2019-04-12 16:53:26 +02:00
"github.com/status-im/status-console-client/protocol/gethservice"
"github.com/status-im/status-go/logutils"
2019-01-22 09:39:23 +01:00
"github.com/status-im/status-go/node"
2019-01-08 21:02:11 +01:00
"github.com/status-im/status-go/params"
2019-01-24 12:39:23 +01:00
"github.com/status-im/status-go/signal"
2019-01-08 21:02:11 +01:00
)
2019-01-24 12:39:23 +01:00
var g * gocui . Gui
2019-04-12 16:53:26 +02:00
var (
fs = flag . NewFlagSet ( "status-term-client" , flag . ExitOnError )
logLevel = fs . String ( "log-level" , "INFO" , "log level" )
2019-01-08 21:02:11 +01:00
2019-04-30 10:24:33 +02:00
keyHex = fs . String ( "keyhex" , "" , "pass a private key in hex" )
noUI = fs . Bool ( "no-ui" , false , "disable UI" )
2019-04-12 16:53:26 +02:00
// flags acting like commands
createKeyPair = fs . Bool ( "create-key-pair" , false , "creates and prints a key pair instead of running" )
2019-05-31 09:50:25 +02:00
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" )
2019-01-08 21:02:11 +01:00
2019-04-12 16:53:26 +02:00
// flags for in-proc node
2019-05-28 10:49:50 +02:00
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" )
2019-01-08 21:02:11 +01:00
2019-04-12 16:53:26 +02:00
// flags for external node
providerURI = fs . String ( "provider" , "" , "an URI pointing at a provider" )
)
2019-01-22 09:39:23 +01:00
2019-04-12 16:53:26 +02:00
func main ( ) {
2019-05-14 19:34:36 +03:00
if err := ff . Parse ( fs , os . Args [ 1 : ] ) ; err != nil {
exitErr ( errors . Wrap ( err , "failed to parse flags" ) )
}
2019-04-12 16:53:26 +02:00
2019-01-08 21:02:11 +01:00
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
2019-01-24 12:39:23 +01:00
2019-05-28 10:49:50 +02:00
fmt . Printf ( "Contact address: %#x\n" , crypto . FromECDSAPub ( & privateKey . PublicKey ) )
2019-01-08 21:02:11 +01:00
} else {
exitErr ( errors . New ( "private key is required" ) )
}
2019-05-28 10:49:50 +02:00
// 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 )
2019-05-08 10:23:51 +03:00
if err != nil {
exitErr ( err )
}
2019-05-28 10:49:50 +02:00
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 )
2019-04-30 10:24:33 +02:00
if err != nil {
exitErr ( err )
}
defer db . Close ( )
2019-05-31 09:50:25 +02:00
// Manage initial contacts.
2019-04-30 10:24:33 +02:00
if contacts , err := db . Contacts ( ) ; len ( contacts ) == 0 || err != nil {
debugContacts := [ ] client . Contact {
2019-05-15 14:00:04 +03:00
{ Name : "status" , Type : client . ContactPublicRoom , Topic : "status" } ,
{ Name : "status-core" , Type : client . ContactPublicRoom , Topic : "status-core" } ,
2019-04-30 10:24:33 +02:00
}
2019-05-22 19:55:34 +03:00
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 )
}
2019-04-30 10:24:33 +02:00
}
}
2019-05-28 09:39:47 +02:00
2019-05-31 09:50:25 +02:00
// 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 . ContactWithPublicKey ( options [ 1 ] , options [ 2 ] )
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 . MessengerV2
)
if * providerURI != "" {
messenger , err = createMessengerWithURI ( * providerURI , privateKey , db )
if err != nil {
exitErr ( err )
}
} else {
messenger , err = createMessengerInProc ( privateKey , db )
if err != nil {
exitErr ( err )
}
}
2019-05-28 09:39:47 +02:00
// run in a goroutine to show the UI faster
2019-05-15 14:00:04 +03:00
go func ( ) {
2019-05-28 09:39:47 +02:00
if err := messenger . Start ( ) ; err != nil {
2019-05-15 14:00:04 +03:00
exitErr ( err )
}
} ( )
2019-01-24 12:39:23 +01:00
2019-05-28 09:39:47 +02:00
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..." )
2019-04-30 10:24:33 +02:00
if ! * noUI {
2019-05-28 09:39:47 +02:00
go func ( ) {
<- done
exitErr ( errors . New ( "exit with signal" ) )
} ( )
2019-04-30 10:24:33 +02:00
if err := setupGUI ( privateKey , messenger ) ; err != nil {
exitErr ( err )
}
2019-01-24 12:39:23 +01:00
2019-04-30 10:24:33 +02:00
if err := g . MainLoop ( ) ; err != nil && err != gocui . ErrQuit {
exitErr ( err )
2019-01-22 09:39:23 +01:00
}
2019-05-28 09:39:47 +02:00
2019-04-30 10:24:33 +02:00
g . Close ( )
} else {
<- done
}
}
2019-04-12 16:53:26 +02:00
2019-04-30 10:24:33 +02:00
func exitErr ( err error ) {
if g != nil {
g . Close ( )
}
2019-01-22 09:39:23 +01:00
2019-04-30 10:24:33 +02:00
fmt . Println ( err )
os . Exit ( 1 )
}
type keysGetter struct {
privateKey * ecdsa . PrivateKey
}
func ( k keysGetter ) PrivateKey ( ) ( * ecdsa . PrivateKey , error ) {
return k . privateKey , nil
}
2019-05-14 19:34:36 +03:00
func createMessengerWithURI ( uri string , pk * ecdsa . PrivateKey , db client . Database ) ( * client . MessengerV2 , error ) {
2019-04-30 10:24:33 +02:00
rpc , err := rpc . Dial ( * providerURI )
if err != nil {
return nil , errors . Wrap ( err , "failed to dial" )
}
// TODO: provide Mail Servers in a different way.
nodeConfig , err := generateStatusNodeConfig ( * dataDir , * fleet , * configFile )
if err != nil {
return nil , errors . Wrap ( err , "failed to generate node config" )
}
proto := adapters . NewWhisperClientAdapter (
rpc ,
pk ,
nodeConfig . ClusterConfig . TrustedMailServers ,
)
2019-05-14 19:34:36 +03:00
messenger := client . NewMessengerV2 ( pk , proto , db )
return & messenger , nil
2019-04-30 10:24:33 +02:00
}
2019-01-22 09:39:23 +01:00
2019-05-14 19:34:36 +03:00
func createMessengerInProc ( pk * ecdsa . PrivateKey , db client . Database ) ( * client . MessengerV2 , error ) {
2019-04-30 10:24:33 +02:00
// collect mail server request signals
signalsForwarder := newSignalForwarder ( )
go signalsForwarder . Start ( )
2019-04-12 16:53:26 +02:00
2019-04-30 10:24:33 +02:00
// setup signals handler
signal . SetDefaultNodeNotificationHandler (
2019-05-08 09:34:59 +03:00
filterMailTypesHandler ( signalsForwarder . in ) ,
2019-04-30 10:24:33 +02:00
)
2019-04-04 17:07:27 +02:00
2019-04-30 10:24:33 +02:00
nodeConfig , err := generateStatusNodeConfig ( * dataDir , * fleet , * configFile )
if err != nil {
exitErr ( errors . Wrap ( err , "failed to generate node config" ) )
}
2019-04-04 17:07:27 +02:00
2019-04-30 10:24:33 +02:00
statusNode := node . New ( )
protocolGethService := gethservice . New (
statusNode ,
& keysGetter { privateKey : pk } ,
)
2019-04-04 17:07:27 +02:00
2019-04-30 10:24:33 +02:00
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" )
}
adapter := adapters . NewWhisperServiceAdapter ( statusNode , shhService , pk )
2019-05-14 19:34:36 +03:00
messenger := client . NewMessengerV2 ( pk , adapter , db )
2019-04-30 10:24:33 +02:00
protocolGethService . SetProtocol ( adapter )
2019-05-14 19:34:36 +03:00
protocolGethService . SetMessenger ( & messenger )
2019-04-30 10:24:33 +02:00
// 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" ) )
}
2019-04-04 17:07:27 +02:00
2019-04-30 10:24:33 +02:00
if err := adapter . InitPFS ( databasesDir ) ; err != nil {
exitErr ( errors . Wrap ( err , "initialize PFS" ) )
2019-04-04 17:07:27 +02:00
}
2019-04-30 10:24:33 +02:00
log . Printf ( "PFS has been initialized" )
2019-01-22 09:39:23 +01:00
}
2019-05-14 19:34:36 +03:00
return & messenger , nil
2019-04-30 10:24:33 +02:00
}
2019-05-14 19:34:36 +03:00
func setupGUI ( privateKey * ecdsa . PrivateKey , messenger * client . MessengerV2 ) error {
2019-04-30 10:24:33 +02:00
var err error
// global
2019-01-24 12:39:23 +01:00
g , err = gocui . NewGui ( gocui . Output256 )
2019-01-08 21:02:11 +01:00
if err != nil {
2019-04-30 10:24:33 +02:00
return err
2019-01-08 21:02:11 +01:00
}
// prepare views
vm := NewViewManager ( nil , g )
2019-03-25 11:01:42 +01:00
notifications := NewNotificationViewController ( & ViewController { vm , g , ViewNotification } )
chat := NewChatViewController (
2019-03-11 18:49:18 +01:00
& ViewController { vm , g , ViewChat } ,
privateKey ,
messenger ,
2019-03-25 11:01:42 +01:00
func ( err error ) {
_ = notifications . Error ( "Chat error" , fmt . Sprintf ( "%v" , err ) )
} ,
2019-03-11 18:49:18 +01:00
)
2019-01-08 21:02:11 +01:00
2019-03-11 18:49:18 +01:00
contacts := NewContactsViewController ( & ViewController { vm , g , ViewContacts } , messenger )
2019-04-19 13:22:18 +02:00
if err := contacts . LoadAndRefresh ( ) ; err != nil {
2019-04-30 10:24:33 +02:00
return err
2019-03-11 18:49:18 +01:00
}
2019-01-08 21:02:11 +01:00
2019-02-25 08:29:35 +01:00
inputMultiplexer := NewInputMultiplexer ( )
inputMultiplexer . AddHandler ( DefaultMultiplexerPrefix , func ( b [ ] byte ) error {
log . Printf ( "default multiplexer handler" )
2019-03-11 18:49:18 +01:00
return chat . Send ( b )
2019-02-25 08:29:35 +01:00
} )
inputMultiplexer . AddHandler ( "/contact" , ContactCmdFactory ( contacts ) )
inputMultiplexer . AddHandler ( "/request" , RequestCmdFactory ( chat ) )
2019-01-08 21:02:11 +01:00
views := [ ] * View {
& View {
Name : ViewContacts ,
2019-03-25 11:01:42 +01:00
Enabled : true ,
2019-01-08 21:02:11 +01:00
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" )
}
2019-03-13 21:20:22 +01:00
// 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
2019-01-08 21:02:11 +01:00
} ) ,
} ,
} ,
} ,
& View {
Name : ViewChat ,
2019-03-25 11:01:42 +01:00
Enabled : true ,
2019-01-08 21:02:11 +01:00
Cursor : true ,
2019-03-11 18:49:18 +01:00
Autoscroll : false ,
2019-01-08 21:02:11 +01:00
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 ,
} ,
2019-03-17 13:20:02 +01:00
Binding {
2019-03-17 21:24:39 +01:00
Key : gocui . KeyHome ,
Mod : gocui . ModNone ,
Handler : func ( g * gocui . Gui , v * gocui . View ) error {
2019-04-30 10:24:33 +02:00
params , err := chat . RequestOptions ( false )
if err != nil {
return err
}
2019-03-17 21:24:39 +01:00
2019-03-25 11:01:42 +01:00
if err := notifications . Debug ( "Messages request" , fmt . Sprintf ( "%v" , params ) ) ; err != nil {
return err
}
2019-03-17 21:24:39 +01:00
// 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 {
2019-03-25 11:01:42 +01:00
_ = notifications . Error ( "Request failed" , fmt . Sprintf ( "%v" , err ) )
2019-03-17 21:24:39 +01:00
}
} ( )
return HomeHandler ( g , v )
} ,
2019-03-17 13:20:02 +01:00
} ,
Binding {
Key : gocui . KeyEnd ,
Mod : gocui . ModNone ,
Handler : EndHandler ,
} ,
2019-01-08 21:02:11 +01:00
} ,
} ,
& View {
Name : ViewInput ,
2019-03-25 11:01:42 +01:00
Enabled : true ,
2019-01-08 21:02:11 +01:00
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 ,
2019-02-25 08:29:35 +01:00
Handler : inputMultiplexer . BindingHandler ,
2019-01-08 21:02:11 +01:00
} ,
Binding {
Key : gocui . KeyEnter ,
Mod : gocui . ModAlt ,
Handler : MoveToNewLineHandler ,
} ,
} ,
} ,
2019-03-25 11:01:42 +01:00
& 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
} ,
} ,
} ,
} ,
2019-01-08 21:02:11 +01:00
}
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 {
2019-04-30 10:24:33 +02:00
return err
2019-01-08 21:02:11 +01:00
}
if err := vm . SetGlobalKeybindings ( bindings ) ; err != nil {
2019-04-30 10:24:33 +02:00
return err
2019-01-08 21:02:11 +01:00
}
2019-04-30 10:24:33 +02:00
return nil
2019-04-12 16:53:26 +02:00
}