all repos — felt @ c6c68aa443563e4d02adc1eb929f88c323192f43

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

add configand command line parsing
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmP7zYoACgkQO3+8IhRO
Y5iDTQ//dbXCsXafhCKE/VZDwN7QtgvIM471Pz7dVwIcxxozVQT/TTNt7XjgecCz
wkzFt4BSuIN/JIwOOAMXiqjSD7lz9I1GaNcLlFuLWKd5Cw8nBr56cU0kja3cA0qN
FR8lxD98Bu6oHceqbzWWZQLnA2kFY6ZKFPTLH6GS/cXimvjcHzae5p1OyIkzXQM1
SO3iRUYUoerRVFC2numyhmD2blhqcg/XVuhWmN6p0va0HK10viJe5sHzkmlludpy
JdnoqQJP++l1amWNjoSTTTduIfemPReonZ4HIjOPCA8+QGnIT5PwIexueIoGTkIA
63CPlLMRGUQdVe5ft1qS/eIIK5+usJdQg70bss1AGN9dlNVbmDz4gga61TEJMzYY
G8+Y2/lW7MZDVxubCsdbSBTQryETXKaFRUY2CxMljXfa7sYmw03uS4dZ5uqFVd3T
tRq0W7hLyxwZyIm71aDNVvp7sE4nDCx34qnWsBvwJjkjSUpLEsCjO4TQF3e8VljN
BnbKcEl4Mx8siMNJgrD6khoadyAp0DCffB3oXnyhgJAhPjgoqa6Xph1nlAXYww+h
eLVAI17VhkGkUtmKgZ0qT+NQGVGFG/XQQYBgpAhui0CFz6VCNovQIQ6OnSxdQLIA
H7d1MR4acqEsN1a459l1Ph5K+7ufHXtT9EYZ4O6SscNqyv9mLJs=
=ZDRE
-----END PGP SIGNATURE-----
commit

c6c68aa443563e4d02adc1eb929f88c323192f43

parent

4ea3e656d687f901f5d30c56d9196e9b99077dcb

5 files changed, 221 insertions(+), 15 deletions(-)

jump to
M admin/admin.goadmin/admin.go

@@ -154,20 +154,28 @@ // write to file

// respond with URL? } - return handlerFunc + return http.HandlerFunc(handlerFunc) } -func CreateAdminInterface(udb auth.UserStore, dbAdapter mongodb.DbAdapter) http.Handler { +func CreateAdminInterface(udb auth.UserStore, dbAdapter mongodb.DbAdapter, uploads string, uploadMaxMB int) http.Handler { // create quartzgun router rtr := &router.Router{Fallback: *template.Must(template.ParseFiles("static/error.html"))} scopes := map[string]string{} rtr.Post("/api/auth/", Provision(udb, 84)) + + // table management rtr.Get("/api/table/", Validate(apiGetTableList(renderer.JSON("tableList"), udb), udb, scopes)) rtr.Get(`/api/table/(?P<Slug>\S+)`, Validate(apiGetTableData(renderer.JSON("tableData"), udb, dbAdapter), udb, scopes)) rtr.Post("/api/table/", Validate(apiCreateTable(renderer.JSON("result"), udb, dbAdapter), udb, scopes)) rtr.Delete(`/api/table/(?P<Slug>\S+)`, Validate(apiDestroyTable(renderer.JSON("result"), udb, dbAdapter), udb, scopes)) + + // asset management + // POST /api/upload/<table>/map/ + // DELETE /api/upload/<table>/map/<map> + // POST /api/upload/<table>/token/ + // DELETE /api/upload/<table>/token/<token> return http.HandlerFunc(rtr.ServeHTTP) }
M cmd/cmd.gocmd/cmd.go

@@ -1,12 +1,12 @@

package cmd import ( - "fmt" + _ "fmt" "hacklab.nilfm.cc/quartzgun/auth" - "strings" + _ "strings" ) -func ProcessCmd(args []string, userStore auth.UserStore, cfg *Config) bool { +func ProcessCmd(args []string, userStore auth.UserStore) bool { if len(args) == 1 { return false }
A config/config.go

@@ -0,0 +1,187 @@

+package config + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" +) + +type Config struct { + Port int + Uploads string + UploadMaxMB int + MongoURI string +} + +func GetConfigLocation() string { + home := os.Getenv("HOME") + appdata := os.Getenv("APPDATA") + switch runtime.GOOS { + case "windows": + return filepath.Join(appdata, "felt") + case "darwin": + return filepath.Join(home, "Library", "Application Support", "felt") + case "plan9": + return filepath.Join(home, "lib", "felt") + default: + return filepath.Join(home, ".config", "felt") + } +} + +func ensureConfigLocationExists() { + fileInfo, err := os.Stat(GetConfigLocation()) + + if os.IsNotExist(err) { + os.MkdirAll(GetConfigLocation(), os.ModePerm) + } else if !fileInfo.IsDir() { + panic("Config location is not a directory!") + } +} + +func ReadConfig() *Config { + ensureConfigLocationExists() + return parseConfig(filepath.Join(GetConfigLocation(), "felt.conf")) +} + +func (self *Config) Write() error { + ensureConfigLocationExists() + return writeConfig(self, filepath.Join(GetConfigLocation(), "felt.conf")) +} + +func (self *Config) IsNull() bool { + return self.Port == 0 || self.UploadMaxMB == 0 || self.Uploads == "" +} + +func (self *Config) RunWizard() { + fmt.Printf("All options are required.\n") + defer func(cfg *Config) { + if r := recover(); r != nil { + fmt.Printf("Invalid selection, starting over...") + cfg.RunWizard() + } + }(self) + inputBuf := "" + + fmt.Printf("MongoDB URI? ") + ensureNonEmptyOption(&inputBuf) + self.MongoURI = inputBuf + + inputBuf = "" + fmt.Printf("TCP port? ") + self.Port = ensurePortOption(&inputBuf) + + fmt.Printf("file uploads location? ") + ensureNonEmptyOption(&inputBuf) + self.Uploads = inputBuf + + inputBuf = "" + fmt.Printf("Max file upload size (MB)? ") + self.UploadMaxMB = ensureNumberOption(&inputBuf) + + fmt.Printf("Configuration complete!\n") + self.Write() +} + +func ensureNonEmptyOption(buffer *string) { + for { + fmt.Scanln(buffer) + if len(strings.TrimSpace(*buffer)) != 0 { + break + } + fmt.Println("Please enter a nonempty value") + } +} + +func ensureBooleanOption(buffer *string) bool { + for { + fmt.Scanln(buffer) + trimmedBuf := strings.TrimSpace(*buffer) + v, err := strconv.ParseBool(trimmedBuf) + if err == nil { + return v + } + fmt.Println("Please enter a true or false value") + } +} + +func ensureNumberOption(buffer *string) int { + for { + fmt.Scanln(buffer) + trimmedBuf := strings.TrimSpace(*buffer) + v, err := strconv.ParseInt(trimmedBuf, 10, 32) + if err == nil && v > 0 { + return int(v) + } + fmt.Println("Please enter a positive integer") + } +} + +func ensurePortOption(buffer *string) int { + for { + fmt.Scanln(buffer) + trimmedBuf := strings.TrimSpace(*buffer) + v, err := strconv.ParseInt(trimmedBuf, 10, 32) + if err == nil && v > 0 && v <= 65536 { + return int(v) + } + fmt.Println("Please enter a valid port [1, 65536]") + } +} + +func writeConfig(cfg *Config, configFile string) error { + f, err := os.Create(configFile) + if err != nil { + return err + } + + defer f.Close() + + f.WriteString("mongoURI=" + cfg.MongoURI + "\n") + f.WriteString("uploads=" + cfg.Uploads + "\n") + f.WriteString("uploadMaxMB=" + strconv.FormatInt(int64(cfg.UploadMaxMB), 10) + "\n") + f.WriteString("port=" + strconv.FormatInt(int64(cfg.Port), 10) + "\n") + return nil +} + +func parseConfig(configFile string) *Config { + f, err := os.ReadFile(configFile) + cfg := &Config{} + if err != nil { + return cfg + } + + fileData := string(f[:]) + + lines := strings.Split(fileData, "\n") + + for _, l := range lines { + if len(l) == 0 { + continue + } + if !strings.Contains(l, "=") { + panic("Malformed config not in INI format") + } + + kvp := strings.Split(l, "=") + k := strings.TrimSpace(kvp[0]) + v := strings.TrimSpace(kvp[1]) + switch k { + case "mongoURI": + cfg.MongoURI = v + case "uploads": + cfg.Uploads = v + case "port": + port, _ := strconv.ParseInt(v, 10, 32) + cfg.Port = int(port) + case "uploadMaxMB": + maxUpload, _ := strconv.ParseInt(v, 10, 32) + cfg.UploadMaxMB = int(maxUpload) + default: + panic("Unrecognized config option: " + k) + } + } + return cfg +}
M gametable/server.gogametable/server.go

@@ -36,7 +36,7 @@ subscribers map[*Subscriber]models.TableKey

dbAdapter mongodb.DbAdapter } -func New(adapter mongodb.DbAdapter, udb auth.UserStore) *GameTableServer { +func New(adapter mongodb.DbAdapter, udb auth.UserStore, uploads string, uploadMaxMB int) *GameTableServer { srvr := &GameTableServer{ subscribeMessageBuffer: 16, logf: log.Printf,

@@ -45,7 +45,7 @@ publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8),

dbAdapter: adapter, } srvr.serveMux.Handle("/table/", http.StripPrefix("/table/", renderer.Subtree("./static"))) - srvr.serveMux.Handle("/admin/", http.StripPrefix("/admin", admin.CreateAdminInterface(udb, adapter))) + srvr.serveMux.Handle("/admin/", http.StripPrefix("/admin", admin.CreateAdminInterface(udb, adapter, uploads, uploadMaxMB))) srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler) srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
M main.gomain.go

@@ -2,6 +2,8 @@ package main

import ( "context" + "hacklab.nilfm.cc/felt/cmd" + "hacklab.nilfm.cc/felt/config" "hacklab.nilfm.cc/felt/gametable" "hacklab.nilfm.cc/felt/mongodb" "hacklab.nilfm.cc/quartzgun/indentalUserDB"

@@ -10,6 +12,8 @@ "net"

"net/http" "os" "os/signal" + "path/filepath" + "strconv" "time" )

@@ -21,23 +25,30 @@ }

} func run() error { - l, err := net.Listen("tcp", os.Args[1]) + cfg := config.ReadConfig() + if cfg.IsNull() { + cfg.RunWizard() + } + + udb := indentalUserDB.CreateIndentalUserDB( + filepath.Join(config.GetConfigLocation(), "user.db")) + + if cmd.ProcessCmd(os.Args, udb) { + os.Exit(0) + } + + l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(cfg.Port), 10)) if err != nil { return err } dbEngine := &mongodb.DbEngine{} - err = dbEngine.Init(os.Args[2]) + err = dbEngine.Init(cfg.MongoURI) if err != nil { return err } - udb := indentalUserDB.CreateIndentalUserDB( - "./user.db") - - udb.AddUser("nilix", "questing") - - gt := gametable.New(dbEngine, udb) + gt := gametable.New(dbEngine, udb, cfg.Uploads, cfg.UploadMaxMB) s := &http.Server{ Handler: gt, ReadTimeout: time.Second * 10,