From 8b35983b220817d14dee53d462e0abbcb927f574 Mon Sep 17 00:00:00 2001 From: Lars Bahner Date: Sun, 7 Apr 2024 04:49:38 +0200 Subject: [PATCH] Structure flag parsing Make a flagset for each discrete config. This makes ordering feasible. Notably Profile() is treadted properly. This way it's possible to avoid race conditions. --- .vscode/launch.json | 23 +++++- cmd/node/actors.go | 4 +- cmd/pong/main.go | 2 +- cmd/relay/config.go | 3 +- cmd/robot/main.go | 3 +- config/actor.go | 130 +++++++++++++++++++++++++++++++--- config/common.go | 116 ++++++++++++++++++++++++++++++ config/config.go | 5 +- config/db.go | 43 ++++++++--- config/flags.go | 75 -------------------- config/generate.go | 2 +- config/help.go | 20 ++++++ config/http.go | 33 ++++++--- config/{logging.go => log.go} | 32 +++++++-- config/p2p.go | 73 +++++++++++++------ config/profile.go | 7 +- config/xdg.go | 19 ++--- entity/actor/actor.go | 50 ++++--------- entity/actor/config.go | 9 +-- entity/actor/document.go | 112 ----------------------------- entity/actor/hello.go | 2 +- entity/actor/init.go | 10 +-- entity/actor/keyset.go | 10 +-- ui/actor.go | 4 +- ui/history.go | 88 +++++++++++++++++++++++ 25 files changed, 540 insertions(+), 335 deletions(-) create mode 100644 config/common.go delete mode 100644 config/flags.go create mode 100644 config/help.go rename config/{logging.go => log.go} (73%) delete mode 100644 entity/actor/document.go create mode 100644 ui/history.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 1f29164..eb53496 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -65,10 +65,27 @@ }, "args": [ "--generate", - "--nick", + "--profile", "FUBAR", - "--publish", - "--force", + "--nick", + "FUBAR" + ] + }, + { + "name": "go-ma-actor-relay generate", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/relay/", + "console": "integratedTerminal", + "env": { + "GOLOG_FILE": "debug.log", + "GOLOG_LOG_LEVEL": "debug", + "GOLOG_STDOUT": "false", + "GOLOG_OUTPUT": "file" + }, + "args": [ + "--generate", ] } ] diff --git a/cmd/node/actors.go b/cmd/node/actors.go index 15faf0b..aaa70e4 100644 --- a/cmd/node/actors.go +++ b/cmd/node/actors.go @@ -40,12 +40,12 @@ func getOrCreateActor(id string) (*actor.Actor, error) { } // Assuming entity.NewFromKeyset returns *actor.Actor - a, err := actor.NewFromKeyset(k) + a, err := actor.New(k) if err != nil { return nil, fmt.Errorf("failed to create entity: %w", err) } - a.Entity.Doc, err = a.CreateEntityDocument(a.Entity.DID.Id) + a.Entity.Doc, err = doc.NewFromKeyset(a.Keyset) if err != nil { return nil, fmt.Errorf("failed to create DID Document: %w", err) } diff --git a/cmd/pong/main.go b/cmd/pong/main.go index aaea641..d80117d 100644 --- a/cmd/pong/main.go +++ b/cmd/pong/main.go @@ -45,7 +45,7 @@ func main() { go handleMessageEvents(ctx, a) fmt.Println("Started event handlers.") - actor.HelloWorld(ctx, a) + a.HelloWorld(ctx) fmt.Println("Sent hello world.") // WEB diff --git a/cmd/relay/config.go b/cmd/relay/config.go index f79cc8c..2e8a2d2 100644 --- a/cmd/relay/config.go +++ b/cmd/relay/config.go @@ -4,7 +4,6 @@ import ( "os" "github.com/bahner/go-ma-actor/config" - "github.com/spf13/pflag" "gopkg.in/yaml.v2" ) @@ -18,8 +17,8 @@ type RelayConfig struct { func initConfig(defaultProfileName string) RelayConfig { - pflag.Parse() config.SetDefaultProfileName(defaultProfileName) + config.ParseCommonFlags(true) config.Init() c := RelayConfig{ diff --git a/cmd/robot/main.go b/cmd/robot/main.go index 323f569..460e2e0 100644 --- a/cmd/robot/main.go +++ b/cmd/robot/main.go @@ -5,7 +5,6 @@ import ( "fmt" "log" - "github.com/bahner/go-ma-actor/entity/actor" "github.com/bahner/go-ma-actor/ui/web" "github.com/bahner/go-ma-actor/p2p" @@ -34,7 +33,7 @@ func main() { log.Fatal(err) } - actor.HelloWorld(ctx, i.Robot) + i.Robot.HelloWorld(ctx) // i.Robot.HelloWorld(ctx, a) fmt.Println("Press Ctrl-C to stop.") diff --git a/config/actor.go b/config/actor.go index a940e34..39b2333 100644 --- a/config/actor.go +++ b/config/actor.go @@ -1,12 +1,20 @@ package config +// This file contains the configuration for the actor package. +// It also somewhat strangeky initialises the identoty and generates a new one if needed. +// This is because it's so low level and the identity is needed for the keyset. + import ( + "context" "fmt" "os" + "sync" + "github.com/bahner/go-ma/api" "github.com/spf13/pflag" "github.com/spf13/viper" + "github.com/bahner/go-ma/did/doc" "github.com/bahner/go-ma/key/set" log "github.com/sirupsen/logrus" ) @@ -17,25 +25,49 @@ const ( ) var ( - keyset set.Keyset + actorKeyset set.Keyset ErrEmptyIdentity = fmt.Errorf("identity is empty") ErrFakeIdentity = fmt.Errorf("your identity is fake. You need to define actorKeyset or generate a new one") ErrEmptyNick = fmt.Errorf("nick is empty") + ActorFlags pflag.FlagSet + actorOnce sync.Once ) // Initialise command line flags for the actor package // The actor is optional for some commands, but required for others. -func ActorFlags() { +// exitOnHelp means that this function is the last called when help is needed. +// and the program should exit. +func ParseActorFlags(exitOnHelp bool) { + + InitCommon() + InitLog() + InitDB() + InitP2P() + InitHTTP() + + actorOnce.Do(func() { - pflag.StringP("nick", "n", "", "Nickname to use in character creation") - pflag.StringP("location", "l", defaultLocation, "DID of the location to visit") + ActorFlags.StringP("nick", "n", "", "Nickname to use in character creation") + ActorFlags.StringP("location", "l", defaultLocation, "DID of the location to visit") - viper.BindPFlag("actor.nick", pflag.Lookup("nick")) - viper.BindPFlag("actor.location", pflag.Lookup("location")) + viper.BindPFlag("actor.nick", ActorFlags.Lookup("nick")) + viper.BindPFlag("actor.location", ActorFlags.Lookup("location")) - viper.SetDefault("actor.location", defaultLocation) - viper.SetDefault("actor.nick", defaultNick()) + viper.SetDefault("actor.location", defaultLocation) + viper.SetDefault("actor.nick", defaultNick()) + if HelpNeeded() { + fmt.Println("Actor Flags:") + ActorFlags.PrintDefaults() + + if exitOnHelp { + os.Exit(0) + } + + } else { + ActorFlags.Parse(os.Args[1:]) + } + }) } type ActorConfig struct { @@ -44,17 +76,25 @@ type ActorConfig struct { Location string `yaml:"location"` } -// Cofig for actor. Remember to parse the flags first. +// Config for actor. Remember to parse the flags first. // Eg. ActorFlags() func Actor() ActorConfig { + // Fetch the identity from the config or generate one identity, err := actorIdentity() if err != nil { panic(err) } + // Unpack the keyset from the identity initActorKeyset(identity) + // If we are generating a new identity we should publish it + if GenerateFlag() { + renameIPNSKey(actorKeyset.DID.Fragment) + publishIdentityFromKeyset(actorKeyset) + } + return ActorConfig{ Identity: identity, Nick: ActorNick(), @@ -62,6 +102,51 @@ func Actor() ActorConfig { } } +func renameIPNSKey(name string) error { + + keyExists := ipnsKeyExists(name) + + if !keyExists { + return nil + } + + // If the key exists and the force flag is not set, return an error + if !ForceFlag() { + return fmt.Errorf("config.renameIPNSKey: force flag not set") + } + + ctx := context.Background() + ipfsAPI := api.GetIPFSAPI() + + backupName := name + "~" + _, _, err := ipfsAPI.Key().Rename(ctx, name, backupName) + + log.Infof("Renamed existing IPNS key: %s to %s", name, backupName) + + return err +} + +func ipnsKeyExists(name string) bool { + + ctx := context.Background() + + ipfsAPI := api.GetIPFSAPI() + + keys, err := ipfsAPI.Key().List(ctx) + if err != nil { + log.Errorf("config.ipnsKeyExists: %v", err) + return false + } + + for _, key := range keys { + if key.Name() == name { + return true + } + } + + return false +} + // Fetches the actor nick from the config or the command line // NB! This is a little more complex than the other config functions, as it // needs to fetch the nick from the command line if it's not in the config. @@ -75,7 +160,7 @@ func ActorLocation() string { } func ActorKeyset() set.Keyset { - return keyset + return actorKeyset } func actorIdentity() (string, error) { @@ -108,7 +193,7 @@ func initActorKeyset(keyset_string string) { os.Exit(64) // EX_USAGE } - keyset, err = set.Unpack(keyset_string) + actorKeyset, err = set.Unpack(keyset_string) if err != nil { log.Errorf("config.initActor: %v", err) os.Exit(70) // EX_SOFTWARE @@ -132,3 +217,26 @@ func generateKeysetString(nick string) (string, error) { return pks, nil } + +func publishIdentityFromKeyset(k set.Keyset) error { + + d, err := doc.NewFromKeyset(k) + if err != nil { + return fmt.Errorf("config.publishIdentityFromKeyset: failed to create DOC: %w", err) + } + + assertionMethod, err := d.GetAssertionMethod() + if err != nil { + return fmt.Errorf("config.publishIdentityFromKeyset: %w", err) + } + d.Sign(k.SigningKey, assertionMethod) + + _, err = d.Publish() + if err != nil { + return fmt.Errorf("config.publishIdentityFromKeyset: %w", err) + + } + log.Debugf("Published identity: %s", d.ID) + + return nil +} diff --git a/config/common.go b/config/common.go new file mode 100644 index 0000000..4feed39 --- /dev/null +++ b/config/common.go @@ -0,0 +1,116 @@ +package config + +import ( + "fmt" + "os" + "sync" + + log "github.com/sirupsen/logrus" + "github.com/spf13/pflag" +) + +var ( + CommonFlags = pflag.NewFlagSet("common", pflag.ExitOnError) + + commonOnce sync.Once +) + +func InitCommon() { + + commonOnce.Do(func() { + + // Allow to set config file via command line flag. + CommonFlags.StringP("config", "c", "", "Config file to use.") + CommonFlags.StringP("profile", "p", "", "Config profile (name) to use.") + + CommonFlags.Bool("show-config", false, "Whether to print the config.") + + CommonFlags.BoolP("version", "v", false, "Print version and exit.") + + CommonFlags.Bool("generate", false, "Generates a new keyset") + CommonFlags.Bool("force", false, "Forces regneration of config keyset and publishing") + + CommonFlags.String("debug-socket", defaultDebugSocket, "Port to listen on for debug endpoints") + + if HelpNeeded() { + fmt.Println("Common flags:") + CommonFlags.PrintDefaults() + } else { + CommonFlags.Parse(os.Args[1:]) + } + }) +} + +func GenerateFlag() bool { + // This will exit when done. It will also publish if applicable. + generateFlag, err := CommonFlags.GetBool("generate") + if err != nil { + log.Warnf("config.init: %v", err) + return false + } + + return generateFlag +} + +func PublishFlag() bool { + publishFlag, err := CommonFlags.GetBool("publish") + if err != nil { + log.Warnf("config.init: %v", err) + return false + } + + return publishFlag +} + +func ShowConfigFlag() bool { + showConfigFlag, err := CommonFlags.GetBool("show-config") + if err != nil { + log.Warnf("config.init: %v", err) + return false + } + + return showConfigFlag +} + +func versionFlag() bool { + versionFlag, err := CommonFlags.GetBool("version") + if err != nil { + log.Warnf("config.init: %v", err) + return false + } + + return versionFlag +} + +func ForceFlag() bool { + forceFlag, err := CommonFlags.GetBool("force") + if err != nil { + log.Warnf("config.init: %v", err) + return false + } + + return forceFlag +} + +/* + Parse common flags. + +This is idemPotent in the sense that it can be called +multiple times without side effects, as the flags are +only parsed once. +Set exitOnHelp to true if you want the program to exit +after help is printed. This is useful for the main function, +when this is the last flag parsing function called. +*/ +func ParseCommonFlags(exitOnHelp bool) { + + InitCommon() + InitLog() + InitDB() + InitP2P() + InitHTTP() + + if HelpNeeded() && exitOnHelp { + os.Exit(0) + } +} diff --git a/config/config.go b/config/config.go index 5eff76c..ef04deb 100644 --- a/config/config.go +++ b/config/config.go @@ -6,7 +6,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - "github.com/spf13/pflag" "github.com/spf13/viper" "gopkg.in/yaml.v2" ) @@ -36,7 +35,7 @@ func Init() error { var err error //VIPER CONFIGURATION - viper.BindPFlag("http.debug-socket", pflag.Lookup("debug-socket")) + viper.BindPFlag("http.debug-socket", CommonFlags.Lookup("debug-socket")) viper.SetDefault("http.debug-socket", defaultDebugSocket) // Read the config file and environment variables. @@ -60,7 +59,7 @@ func Init() error { viper.SetConfigFile(File()) err = viper.ReadInConfig() if err != nil { - log.Warnf("No config file found: %s", err) + panic(err) } } diff --git a/config/db.go b/config/db.go index 911adfa..8d3d83a 100644 --- a/config/db.go +++ b/config/db.go @@ -1,6 +1,10 @@ package config import ( + "fmt" + "os" + "sync" + "github.com/bahner/go-ma-actor/internal" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -11,21 +15,34 @@ const CSVMode = 0664 var ( defaultPeersPath = internal.NormalisePath(dataHome + "/peers.csv") defaultEntitiesPath = internal.NormalisePath(dataHome + "/entities.csv") - defaultHistoryPath = internal.NormalisePath(dataHome + "/" + Profile() + ".history") + dbFlags pflag.FlagSet + dbOnce sync.Once ) -func init() { - pflag.String("peers", defaultPeersPath, "Filename for CSV peers file.") - pflag.String("entities", defaultEntitiesPath, "Filename for CSV entities file.") - pflag.String("history", defaultHistoryPath, "Filename for CSV history file.") +func InitDB() { + + dbOnce.Do(func() { + + dbFlags.String("peers", defaultPeersPath, "Filename for CSV peers file.") + dbFlags.String("entities", defaultEntitiesPath, "Filename for CSV entities file.") + dbFlags.String("history", defaultHistoryPath(), "Filename for CSV history file.") + + viper.BindPFlag("db.peers", dbFlags.Lookup("peers")) + viper.BindPFlag("db.entities", dbFlags.Lookup("entities")) + viper.BindPFlag("db.history", dbFlags.Lookup("history")) - viper.BindPFlag("db.peers", pflag.Lookup("peers")) - viper.BindPFlag("db.entities", pflag.Lookup("entities")) - viper.BindPFlag("db.history", pflag.Lookup("history")) + viper.SetDefault("db.peers", defaultPeersPath) + viper.SetDefault("db.entities", defaultEntitiesPath) + viper.SetDefault("db.history", defaultHistoryPath()) + + if HelpNeeded() { + fmt.Println("DB Flags:") + dbFlags.PrintDefaults() + } else { + dbFlags.Parse(os.Args[1:]) + } + }) - viper.SetDefault("db.peers", defaultPeersPath) - viper.SetDefault("db.entities", defaultEntitiesPath) - viper.SetDefault("db.history", defaultHistoryPath) } type DBConfig struct { @@ -55,3 +72,7 @@ func DBEntities() string { func DBHistory() string { return viper.GetString("db.history") } + +func defaultHistoryPath() string { + return internal.NormalisePath(dataHome + "/" + Profile() + ".history") +} diff --git a/config/flags.go b/config/flags.go deleted file mode 100644 index 85f25e2..0000000 --- a/config/flags.go +++ /dev/null @@ -1,75 +0,0 @@ -package config - -import ( - log "github.com/sirupsen/logrus" - "github.com/spf13/pflag" -) - -func init() { - - // Allow to set config file via command line flag. - pflag.StringP("config", "c", "", "Config file to use.") - pflag.StringP("profile", "p", "", "Config profile (name) to use.") - - pflag.Bool("show-config", false, "Whether to print the config.") - - pflag.BoolP("version", "v", false, "Print version and exit.") - - pflag.Bool("generate", false, "Generates a new keyset") - pflag.Bool("publish", false, "Publishes keyset to IPFS") - pflag.Bool("force", false, "Forces regneration of config keyset and publishing") - - pflag.String("debug-socket", defaultDebugSocket, "Port to listen on for debug endpoints") - -} - -func GenerateFlag() bool { - // This will exit when done. It will also publish if applicable. - generateFlag, err := pflag.CommandLine.GetBool("generate") - if err != nil { - log.Warnf("config.init: %v", err) - return false - } - - return generateFlag -} - -func PublishFlag() bool { - publishFlag, err := pflag.CommandLine.GetBool("publish") - if err != nil { - log.Warnf("config.init: %v", err) - return false - } - - return publishFlag -} - -func ShowConfigFlag() bool { - showConfigFlag, err := pflag.CommandLine.GetBool("show-config") - if err != nil { - log.Warnf("config.init: %v", err) - return false - } - - return showConfigFlag -} - -func versionFlag() bool { - versionFlag, err := pflag.CommandLine.GetBool("version") - if err != nil { - log.Warnf("config.init: %v", err) - return false - } - - return versionFlag -} - -func ForceFlag() bool { - forceFlag, err := pflag.CommandLine.GetBool("force") - if err != nil { - log.Warnf("config.init: %v", err) - return false - } - - return forceFlag -} diff --git a/config/generate.go b/config/generate.go index 575d735..8eebc7d 100644 --- a/config/generate.go +++ b/config/generate.go @@ -54,7 +54,7 @@ func writeGeneratedConfigFile(content []byte) { } else { errMsg = fmt.Sprintf("Failed to open file: %v", err) } - log.Fatalf(errMsg) + panic(errMsg) } defer file.Close() diff --git a/config/help.go b/config/help.go new file mode 100644 index 0000000..eb38f58 --- /dev/null +++ b/config/help.go @@ -0,0 +1,20 @@ +package config + +import ( + "os" +) + +var helpNeeded bool = false + +func init() { + for _, arg := range os.Args[1:] { + if arg == "-h" || arg == "--help" { + helpNeeded = true + return + } + } +} + +func HelpNeeded() bool { + return helpNeeded +} diff --git a/config/http.go b/config/http.go index 6195439..4298d39 100644 --- a/config/http.go +++ b/config/http.go @@ -1,6 +1,10 @@ package config import ( + "fmt" + "os" + "sync" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -10,27 +14,40 @@ const ( defaultHttpRefresh int = 10 ) +var ( + httpFlags pflag.FlagSet + httpOnce sync.Once +) + type HTTPConfig struct { Socket string `yaml:"socket"` Refresh int `yaml:"refresh"` DebugSocket string `yaml:"debug_socket"` } -func init() { +func InitHTTP() { - pflag.String("http-socket", defaultHttpSocket, "Address for webserver to listen on") - pflag.Int("http-refresh", defaultHttpRefresh, "Number of seconds for webpages to wait before refresh") + httpOnce.Do(func() { + httpFlags.String("http-socket", defaultHttpSocket, "Address for webserver to listen on") + httpFlags.Int("http-refresh", defaultHttpRefresh, "Number of seconds for webpages to wait before refresh") - viper.BindPFlag("http.socket", pflag.Lookup("http-socket")) - viper.BindPFlag("http.refresh", pflag.Lookup("http-refresh")) + viper.BindPFlag("http.socket", httpFlags.Lookup("http-socket")) + viper.BindPFlag("http.refresh", httpFlags.Lookup("http-refresh")) + viper.SetDefault("http.socket", defaultHttpSocket) + viper.SetDefault("http.refresh", defaultHttpRefresh) + + if HelpNeeded() { + fmt.Println("HTTP Flags:") + httpFlags.PrintDefaults() + } else { + httpFlags.Parse(os.Args[1:]) + } + }) } func HTTP() HTTPConfig { - viper.SetDefault("http.socket", defaultHttpSocket) - viper.SetDefault("http.refresh", defaultHttpRefresh) - return HTTPConfig{ Socket: HttpSocket(), Refresh: HttpRefresh(), diff --git a/config/logging.go b/config/log.go similarity index 73% rename from config/logging.go rename to config/log.go index aa31991..6ea32ee 100644 --- a/config/logging.go +++ b/config/log.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sync" "github.com/bahner/go-ma-actor/internal" "github.com/mitchellh/go-homedir" @@ -18,16 +19,31 @@ const ( logFilePerm os.FileMode = 0640 ) -var defaultLogfile string = internal.NormalisePath(dataHome + Profile() + ".log") +var ( + logFlags pflag.FlagSet + logOnce sync.Once +) + +func InitLog() { -func init() { + logOnce.Do(func() { + logFlags.String("loglevel", defaultLogLevel, "Loglevel to use for application.") + logFlags.String("logfile", defaultLogfile(), "Logfile to use for application. Accepts 'STDERR' and 'STDOUT' as such.") - pflag.String("loglevel", defaultLogLevel, "Loglevel to use for application.") - pflag.String("logfile", defaultLogfile, "Logfile to use for application. Accepts 'STDERR' and 'STDOUT' as such.") + viper.BindPFlag("log.file", logFlags.Lookup("logfile")) + viper.BindPFlag("log.level", logFlags.Lookup("loglevel")) - viper.BindPFlag("log.file", pflag.Lookup("logfile")) - viper.BindPFlag("log.level", pflag.Lookup("loglevel")) + viper.SetDefault("log.level", defaultLogLevel) + viper.SetDefault("log.file", defaultLogfile) + + if HelpNeeded() { + fmt.Println("Log Flags:") + logFlags.PrintDefaults() + } else { + logFlags.Parse(os.Args[1:]) + } + }) } type LogConfig struct { @@ -55,6 +71,10 @@ func LogFile() string { return viper.GetString("log.file") } +func defaultLogfile() string { + return internal.NormalisePath(dataHome + Profile() + ".log") +} + func initLogging() { viper.SetDefault("log.level", defaultLogLevel) diff --git a/config/p2p.go b/config/p2p.go index 3a2cfaa..8d87546 100644 --- a/config/p2p.go +++ b/config/p2p.go @@ -1,7 +1,10 @@ package config import ( + "fmt" + "os" "strconv" + "sync" "time" "github.com/spf13/pflag" @@ -25,28 +28,54 @@ const ( defaultMDNS bool = true ) -func init() { - - pflag.Bool("dht", defaultDHT, "Whether to discover using DHT") - pflag.Bool("mdns", defaultMDNS, "Whether to discover using MDNS") - pflag.Duration("connmgr-grace-period", defaultConnmgrGracePeriod, "Grace period for connection manager.") - pflag.Duration("discovery-advertise-interval", defaultDiscoveryAdvertiseInterval, "How often to advertise our presence to libp2p") - pflag.Duration("discovery-advertise-ttl", defaultDiscoveryAdvertiseTTL, "Hint of TimeToLive for advertising peer discovery.") - pflag.Int("connmgr-high-watermark", defaultConnmgrHighWatermark, "High watermark for peer discovery.") - pflag.Int("connmgr-low-watermark", defaultConnmgrLowWatermark, "Low watermark for peer discovery.") - pflag.Int("discovery-advertise-limit", defaultDiscoveryAdvertiseLimit, "Limit for advertising peer discovery.") - pflag.Int("port", defaultListenPort, "Port for libp2p node to listen on.") - - // Bind pflags - viper.BindPFlag("p2p.connmgr.grace-period", pflag.Lookup("connmgr-grace-period")) - viper.BindPFlag("p2p.connmgr.high-watermark", pflag.Lookup("connmgr-high-watermark")) - viper.BindPFlag("p2p.connmgr.low-watermark", pflag.Lookup("connmgr-low-watermark")) - viper.BindPFlag("p2p.discovery.advertise-interval", pflag.Lookup("discovery-advertise-interval")) - viper.BindPFlag("p2p.discovery.advertise-limit", pflag.Lookup("discovery-advertise-limit")) - viper.BindPFlag("p2p.discovery.advertise-ttl", pflag.Lookup("discovery-advertise-ttl")) - viper.BindPFlag("p2p.discovery.dht", pflag.Lookup("dht")) - viper.BindPFlag("p2p.discovery.mdns", pflag.Lookup("mdns")) - viper.BindPFlag("p2p.port", pflag.Lookup("port")) +var ( + p2pFlags pflag.FlagSet + p2pOnce sync.Once +) + +func InitP2P() { + + p2pOnce.Do(func() { + + p2pFlags.Bool("dht", defaultDHT, "Whether to discover using DHT") + p2pFlags.Bool("mdns", defaultMDNS, "Whether to discover using MDNS") + p2pFlags.Duration("connmgr-grace-period", defaultConnmgrGracePeriod, "Grace period for connection manager.") + p2pFlags.Duration("discovery-advertise-interval", defaultDiscoveryAdvertiseInterval, "How often to advertise our presence to libp2p") + p2pFlags.Duration("discovery-advertise-ttl", defaultDiscoveryAdvertiseTTL, "Hint of TimeToLive for advertising peer discovery.") + p2pFlags.Int("connmgr-high-watermark", defaultConnmgrHighWatermark, "High watermark for peer discovery.") + p2pFlags.Int("connmgr-low-watermark", defaultConnmgrLowWatermark, "Low watermark for peer discovery.") + p2pFlags.Int("discovery-advertise-limit", defaultDiscoveryAdvertiseLimit, "Limit for advertising peer discovery.") + p2pFlags.Int("port", defaultListenPort, "Port for libp2p node to listen on.") + + // Bind p2pFlagss + viper.BindPFlag("p2p.connmgr.grace-period", p2pFlags.Lookup("connmgr-grace-period")) + viper.BindPFlag("p2p.connmgr.high-watermark", p2pFlags.Lookup("connmgr-high-watermark")) + viper.BindPFlag("p2p.connmgr.low-watermark", p2pFlags.Lookup("connmgr-low-watermark")) + viper.BindPFlag("p2p.discovery.advertise-interval", p2pFlags.Lookup("discovery-advertise-interval")) + viper.BindPFlag("p2p.discovery.advertise-limit", p2pFlags.Lookup("discovery-advertise-limit")) + viper.BindPFlag("p2p.discovery.advertise-ttl", p2pFlags.Lookup("discovery-advertise-ttl")) + viper.BindPFlag("p2p.discovery.dht", p2pFlags.Lookup("dht")) + viper.BindPFlag("p2p.discovery.mdns", p2pFlags.Lookup("mdns")) + viper.BindPFlag("p2p.port", p2pFlags.Lookup("port")) + + viper.SetDefault("p2p.connmgr.grace-period", defaultConnmgrGracePeriod) + viper.SetDefault("p2p.connmgr.high-watermark", defaultConnmgrHighWatermark) + viper.SetDefault("p2p.connmgr.low-watermark", defaultConnmgrLowWatermark) + viper.SetDefault("p2p.discovery.advertise-interval", defaultDiscoveryAdvertiseInterval) + viper.SetDefault("p2p.discovery.advertise-limit", defaultDiscoveryAdvertiseLimit) + viper.SetDefault("p2p.discovery.advertise-ttl", defaultDiscoveryAdvertiseTTL) + viper.SetDefault("p2p.discovery.dht", defaultDHT) + viper.SetDefault("p2p.discovery.mdns", defaultMDNS) + viper.SetDefault("p2p.port", defaultListenPort) + + if HelpNeeded() { + fmt.Println("P2P Flags:") + p2pFlags.PrintDefaults() + } else { + p2pFlags.Parse(os.Args[1:]) + } + }) + } type ConnmgrStruct struct { diff --git a/config/profile.go b/config/profile.go index 7b75dc5..12b71ba 100644 --- a/config/profile.go +++ b/config/profile.go @@ -1,13 +1,16 @@ package config -import "github.com/spf13/pflag" +import ( + log "github.com/sirupsen/logrus" +) var defaultProfile = "actor" // Profile is the mode unless overridden by the profile flag. func Profile() string { - flag := pflag.Lookup("profile") + flag := CommonFlags.Lookup("profile") + log.Debugf("config.Profile: Lookup profile: %v", flag) if flag != nil && flag.Changed { return flag.Value.String() } diff --git a/config/xdg.go b/config/xdg.go index d75c990..e206d6a 100644 --- a/config/xdg.go +++ b/config/xdg.go @@ -8,14 +8,11 @@ import ( "github.com/bahner/go-ma" "github.com/bahner/go-ma-actor/internal" "github.com/mitchellh/go-homedir" - log "github.com/sirupsen/logrus" - "github.com/spf13/pflag" ) var ( - configHome string = xdg.ConfigHome + "/" + ma.NAME + "/" - dataHome string = xdg.DataHome + "/" + ma.NAME + "/" - defaultConfigFile string = internal.NormalisePath(configHome + Profile() + ".yaml") + configHome string = xdg.ConfigHome + "/" + ma.NAME + "/" + dataHome string = xdg.DataHome + "/" + ma.NAME + "/" ) // Returns the configfile name to use. @@ -28,16 +25,16 @@ func File() string { err error ) - config, err := pflag.CommandLine.GetString("config") + config, err := CommonFlags.GetString("config") if err != nil { - log.Fatal(err) + panic(err) } // Prefer explicitly requested config. If not, use the name of the profile name. - if config != defaultConfigFile && config != "" { + if config != defaultConfigFile() && config != "" { filename, err = homedir.Expand(config) if err != nil { - log.Fatal(err) + panic(err) } } else { filename = configHome + Profile() + ".yaml" @@ -47,6 +44,10 @@ func File() string { } +func defaultConfigFile() string { + return internal.NormalisePath(configHome + Profile() + ".yaml") +} + func XDGConfigHome() string { return configHome } diff --git a/entity/actor/actor.go b/entity/actor/actor.go index c6e70f5..da97fdd 100644 --- a/entity/actor/actor.go +++ b/entity/actor/actor.go @@ -5,6 +5,7 @@ import ( "github.com/bahner/go-ma-actor/entity" "github.com/bahner/go-ma/did" + "github.com/bahner/go-ma/did/doc" "github.com/bahner/go-ma/key/set" "github.com/bahner/go-ma/msg" ) @@ -25,14 +26,14 @@ type Actor struct { // Create a new entity from a DID and a Keyset. We need both. // The DID is to verify the entity, and the keyset is to create the // DID Document. -func New(d did.DID, k set.Keyset) (*Actor, error) { +func New(k set.Keyset) (*Actor, error) { err := k.Verify() if err != nil { return nil, fmt.Errorf("entity/new: failed to verify keyset: %w", err) } - e, err := entity.New(d) + e, err := entity.New(k.DID) if err != nil { return nil, err } @@ -43,64 +44,37 @@ func New(d did.DID, k set.Keyset) (*Actor, error) { Envelopes: make(chan *msg.Envelope, ENVELOPES_BUFFERSIZE), } - a.Entity.Doc, err = a.CreateEntityDocument(d.Id) + a.Entity.Doc, err = doc.NewFromKeyset(a.Keyset) if err != nil { panic(err) } - a.Entity.Doc.Publish() - store(a) return a, nil } -// Create a new entity from a DID and use fragment as nick. -func NewFromDID(id string, nick string) (*Actor, error) { - - d, err := did.New(id) - if err != nil { - return nil, fmt.Errorf("entity/newfromdid: failed to create did from ipnsKey: %w", err) - } - - k, err := set.GetOrCreate(d.Fragment) - if err != nil { - return nil, fmt.Errorf("entity/newfromdid: failed to get or create keyset: %w", err) - } - - return New(d, k) -} - // // Get an entity from the global map. // // The input is a full did string. If one is created it will have no Nick. // // The function should do the required lookups to get the nick. // // And verify the entity. func GetOrCreate(id string) (*Actor, error) { - if id == "" { - return nil, fmt.Errorf("entity/getorcreate: empty id") - } - - if !did.IsValid(id) { - return nil, fmt.Errorf("entity/getorcreate: invalid id") + // Creating a DID here implies validation before we try to load the actor. + d, err := did.New(id) + if err != nil { + return nil, fmt.Errorf("actor.GetOrCreate: %w", err) } - var err error - - e := load(id) + e := load(d.Id) if e != nil { return e, nil } - e, err = NewFromDID(id, "") - if err != nil { - return nil, fmt.Errorf("entity/getorcreate: failed to create entity: %w", err) - } - - err = e.Verify() + k, err := set.GetOrCreate(d.Fragment) if err != nil { - return nil, fmt.Errorf("entity/getorcreate: failed to verify created entity: %w", err) + return nil, fmt.Errorf("entity/newfromdid: failed to get or create keyset: %w", err) } - return e, nil + return New(k) } diff --git a/entity/actor/config.go b/entity/actor/config.go index 892f990..9eba31b 100644 --- a/entity/actor/config.go +++ b/entity/actor/config.go @@ -2,7 +2,6 @@ package actor import ( "github.com/bahner/go-ma-actor/config" - "github.com/spf13/pflag" "gopkg.in/yaml.v2" ) @@ -16,13 +15,11 @@ type ActorConfig struct { } // This is an all-inclusive configuration function that sets up the configuration for the actor. -// flags and everything. It is used in the main function of siple actors programmes. -// Remebmer to call check the config.GenerateFlag() and save the configuration if it is set. +// flags and everything. It is used in the main function of simple actors programmes. +// It also parses the common flags. func Config() ActorConfig { - config.ActorFlags() - pflag.Parse() - + config.ParseActorFlags(true) config.Init() return ActorConfig{ diff --git a/entity/actor/document.go b/entity/actor/document.go deleted file mode 100644 index 5833501..0000000 --- a/entity/actor/document.go +++ /dev/null @@ -1,112 +0,0 @@ -package actor - -import ( - "fmt" - - "github.com/bahner/go-ma/did/doc" - "github.com/bahner/go-ma/key" - log "github.com/sirupsen/logrus" -) - -// Creates a ne DID Document for the entity. This only applies if -// the entity has a keyset. If the controller is "", the entity -// is the controller alone. -// NB! All entities must have a document, hence we apply this to the Entity struct, -// but we can only create Documents for actors. For entities we fetch them. -func (a *Actor) CreateEntityDocument(controller string) (*doc.Document, error) { - - id := a.Entity.DID.Id - - if controller == "" { - controller = id - } - - err := a.Keyset.Verify() - if err != nil { - return nil, fmt.Errorf("entity/document: failed to verify keyset: %s", err) - } - - // Initialize a new DID Document - - myDoc, err := doc.New(id, controller) - if err != nil { - return nil, fmt.Errorf("doc/GetOrCreate: failed to create new document: %w", err) - } - - // Add the encryption key to the document, - // and set it as the key agreement key. - log.Debugf("entity/document: existing keyAgreement: %v", myDoc.KeyAgreement) - myEncVM, err := doc.NewVerificationMethod( - id, - id, - key.KEY_AGREEMENT_KEY_TYPE, - a.Keyset.EncryptionKey.DID.Fragment, - a.Keyset.EncryptionKey.PublicKeyMultibase) - if err != nil { - return nil, fmt.Errorf("entity/document: failed to create encryption verification method: %s", err) - } - // Add the controller to the verification method - err = myEncVM.AddController(controller) - if err != nil { - return nil, fmt.Errorf("entity/document: failed to add controller to encryption verification method: %s", err) - } - - // Set the key agreement key verification method - err = myDoc.AddVerificationMethod(myEncVM) - if err != nil { - return nil, fmt.Errorf("entity/document: failed to add encryption verification method to document: %s", err) - } - - myDoc.KeyAgreement = myEncVM.ID - log.Debugf("entity/document: set keyAgreement to %v for %s", myDoc.KeyAgreement, myDoc.ID) - - // Add the signing key to the document and set it as the assertion method. - log.Debugf("entity/document: Creating assertionMethod for document %s", myDoc.ID) - mySignVM, err := doc.NewVerificationMethod( - id, - id, - key.ASSERTION_METHOD_KEY_TYPE, - a.Keyset.SigningKey.DID.Fragment, - a.Keyset.SigningKey.PublicKeyMultibase) - if err != nil { - return nil, fmt.Errorf("entity: failed to create signing verification method: %s", err) - } - // Add the controller to the verification method if applicable - err = mySignVM.AddController(controller) - if err != nil { - return nil, fmt.Errorf("entity: failed to add controller to signing verification method: %s", err) - } - - // Set the assertion method verification method - err = myDoc.AddVerificationMethod(mySignVM) - if err != nil { - return nil, fmt.Errorf("entity: failed to add signing verification method to document: %s", err) - } - - myDoc.AssertionMethod = mySignVM.ID - log.Debugf("entity/document: Set assertionMethod to %v for %s", myDoc.AssertionMethod, mySignVM.ID) - - // Finally sign the document with the signing key. - err = myDoc.Sign(a.Keyset.SigningKey, mySignVM) - if err != nil { - return nil, fmt.Errorf("entity: failed to sign document: %s", err) - } - - return myDoc, nil - -} - -// Creates a new DID Document for the entity, and sets it as the entity's document. -// This only applies if the entity has a keyset. If the controller is "", the entity -// is the controller alone. -func (a *Actor) CreateAndSetEntityDocument(controller string) error { - - doc, err := a.CreateEntityDocument(controller) - if err != nil { - return fmt.Errorf("entity: failed to create document: %s", err) - } - - a.Entity.Doc = doc - return nil - -} diff --git a/entity/actor/hello.go b/entity/actor/hello.go index bb4e036..c0251f8 100644 --- a/entity/actor/hello.go +++ b/entity/actor/hello.go @@ -11,7 +11,7 @@ import ( const broadcastWait = 3 * time.Second -func HelloWorld(ctx context.Context, a *Actor) { +func (a *Actor) HelloWorld(ctx context.Context) { topic, err := pubsub.GetOrCreateTopic(ma.BROADCAST_TOPIC) if err != nil { diff --git a/entity/actor/init.go b/entity/actor/init.go index 5a51db8..3eefeb7 100644 --- a/entity/actor/init.go +++ b/entity/actor/init.go @@ -14,22 +14,16 @@ import ( func Init() *Actor { // The actor is needed for initialisation of the WebHandler. fmt.Println("Creating actor from keyset...") - a, err := NewFromKeyset(config.ActorKeyset()) + a, err := New(config.ActorKeyset()) if err != nil { panic(fmt.Sprintf("error creating actor: %s", err)) } id := a.Entity.DID.Id - fmt.Println("Creating and setting DID Document for actor...") - err = a.CreateAndSetEntityDocument(id) - if err != nil { - panic(fmt.Sprintf("error creating document: %s", err)) - } - // Better safe than sorry. // Without a valid actor, we can't do anything. - if a == nil || a.Verify() != nil { + if a.Verify() != nil { panic(fmt.Sprintf("%s is not a valid actor: %v", id, err)) } diff --git a/entity/actor/keyset.go b/entity/actor/keyset.go index a9324be..3e3e96e 100644 --- a/entity/actor/keyset.go +++ b/entity/actor/keyset.go @@ -6,14 +6,6 @@ import ( "github.com/bahner/go-ma/key/set" ) -// Takes a keyset and an alias (name) and creates a new entity. -// The keyset is used to create the encryption and signing keys. -// The alias can be "" and will be set to the fragment of the DID. -func NewFromKeyset(k set.Keyset) (*Actor, error) { - - return New(k.DID, k) -} - func NewFromPackedKeyset(data string, cached bool) (*Actor, error) { keyset, err := set.Unpack(data) @@ -21,6 +13,6 @@ func NewFromPackedKeyset(data string, cached bool) (*Actor, error) { return nil, fmt.Errorf("entity: failed to unpack keyset: %s", err) } - return NewFromKeyset(keyset) + return New(keyset) } diff --git a/ui/actor.go b/ui/actor.go index 3b56291..be35035 100644 --- a/ui/actor.go +++ b/ui/actor.go @@ -2,8 +2,6 @@ package ui import ( "context" - - "github.com/bahner/go-ma-actor/entity/actor" ) func (ui *ChatUI) startActor() { @@ -18,5 +16,5 @@ func (ui *ChatUI) startActor() { go ui.a.HandleIncomingEnvelopes(ui.currentActorCtx, ui.chMessages) go ui.a.Entity.HandleIncomingMessages(ui.currentActorCtx, ui.chMessages) - go actor.HelloWorld(ui.currentActorCtx, ui.a) // This wait a bit before sending the message. + go ui.a.HelloWorld(ui.currentActorCtx) // This waits a bit before sending the message. } diff --git a/ui/history.go b/ui/history.go new file mode 100644 index 0000000..1c3a418 --- /dev/null +++ b/ui/history.go @@ -0,0 +1,88 @@ +package ui + +import ( + "bufio" + "fmt" + "os" + + "strings" + + "github.com/bahner/go-ma-actor/config" + "github.com/spf13/viper" +) + +func historySize() int { + return viper.GetInt("ui.history-size") +} + +func (ui *ChatUI) pushToHistory(line string) { + + historySize := historySize() + + if len(ui.inputHistory) == historySize { + // Remove the oldest entry when we reach max size + copy(ui.inputHistory, ui.inputHistory[1:]) + ui.inputHistory = ui.inputHistory[:historySize-1] + } + ui.inputHistory = append(ui.inputHistory, line) + if err := appendToPersistentHistory(line + "\n"); err != nil { + fmt.Printf("Error appending to history file: %v\n", err) + } +} + +// appendToFile opens the specified file in append mode and writes the string to it. +func appendToPersistentHistory(text string) error { + + if !(strings.HasPrefix(text, "/") || + strings.HasPrefix(text, "@")) { + return nil + } + + filename := config.DBHistory() + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return err + } + defer file.Close() + + if _, err := file.WriteString(text); err != nil { + return err + } + + return nil +} + +// loadHistory loads the last 'historySize' lines from the history file into the input history. +func (ui *ChatUI) loadHistory() error { + filename := config.DBHistory() + + // Open the file for reading. + file, err := os.Open(filename) + if err != nil { + if os.IsNotExist(err) { + // It's okay if the file doesn't exist yet. + return nil + } + return err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + + // Read all lines into 'lines'. This is not memory efficient for large files but is simple. + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return err + } + + if len(lines) > historySize() { + lines = lines[len(lines)-historySize():] + } + + ui.inputHistory = lines + return nil +}