mirror of https://github.com/dirtbags/moth.git
Mostly refactored
This commit is contained in:
parent
54ea337447
commit
309432d05c
|
@ -36,7 +36,7 @@ And point a browser to http://localhost:8080/ (or whatever host is running the s
|
||||||
The development server includes a number of Python libraries that we have found useful in writing puzzles.
|
The development server includes a number of Python libraries that we have found useful in writing puzzles.
|
||||||
|
|
||||||
When you're ready to create your own puzzles,
|
When you're ready to create your own puzzles,
|
||||||
read [the devel server documentation](docs/devel-server.md).
|
read [the devel server documentation](doc/devel-server.md).
|
||||||
|
|
||||||
Click the `[mb]` link by a puzzle category to compile and download a mothball that the production server can read.
|
Click the `[mb]` link by a puzzle category to compile and download a mothball that the production server can read.
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ Running a Production Server
|
||||||
You can be more fine-grained about directories, if you like.
|
You can be more fine-grained about directories, if you like.
|
||||||
Inside the container, you need the following paths:
|
Inside the container, you need the following paths:
|
||||||
|
|
||||||
* `/state` (rw) Where state is stored. Read [the overview](docs/overview.md) to learn what's what in here.
|
* `/state` (rw) Where state is stored. Read [the overview](doc/overview.md) to learn what's what in here.
|
||||||
* `/mothballs` (ro) Mothballs (puzzle bundles) as provided by the development server.
|
* `/mothballs` (ro) Mothballs (puzzle bundles) as provided by the development server.
|
||||||
* `/resources` (ro) Overrides for built-in HTML/CSS resources.
|
* `/resources` (ro) Overrides for built-in HTML/CSS resources.
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ Point a web browser at http://localhost:8080/
|
||||||
and start hacking on things in your `puzzles` directory.
|
and start hacking on things in your `puzzles` directory.
|
||||||
|
|
||||||
More on how the devel sever works in
|
More on how the devel sever works in
|
||||||
[the devel server documentation](docs/devel-server.md)
|
[the devel server documentation](doc/devel-server.md)
|
||||||
|
|
||||||
|
|
||||||
Running A Production Server
|
Running A Production Server
|
||||||
|
|
|
@ -3,17 +3,26 @@ package main
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MothPath(base string, parts ...string) string {
|
type Component struct {
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) path(parts ...string) string {
|
||||||
path := filepath.Clean(filepath.Join(parts...))
|
path := filepath.Clean(filepath.Join(parts...))
|
||||||
parts = filepath.SplitList(path)
|
parts = filepath.SplitList(path)
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
part = strings.TrimLeft(part, "./\\:")
|
part = strings.TrimLeft(part, "./\\:")
|
||||||
parts[i] = part
|
parts[i] = part
|
||||||
}
|
}
|
||||||
parts = append([]string{base}, parts...)
|
parts = append([]string{c.baseDir}, parts...)
|
||||||
path = filepath.Join(parts...)
|
path = filepath.Join(parts...)
|
||||||
path = filepath.Clean(path)
|
path = filepath.Clean(path)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Component) Run(updateInterval time.Duration) {
|
||||||
|
// Stub!
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Print("Started")
|
||||||
|
|
||||||
|
theme := NewTheme("../../theme")
|
||||||
|
state := NewState("../../state")
|
||||||
|
puzzles := NewMothballs("../../mothballs")
|
||||||
|
|
||||||
|
|
||||||
|
interval := 2 * time.Second
|
||||||
|
go theme.Run(interval)
|
||||||
|
go state.Run(interval)
|
||||||
|
go puzzles.Run(interval)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
log.Print(state.Export(""))
|
||||||
|
time.Sleep(19 * time.Second)
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mothballs struct {
|
||||||
|
Component
|
||||||
|
categories map[string]*Zipfs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMothballs(baseDir string) *Mothballs {
|
||||||
|
return &Mothballs{
|
||||||
|
Component: Component{
|
||||||
|
baseDir: baseDir,
|
||||||
|
},
|
||||||
|
categories: make(map[string]*Zipfs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mothballs) update() {
|
||||||
|
// Any new categories?
|
||||||
|
files, err := ioutil.ReadDir(m.path())
|
||||||
|
if err != nil {
|
||||||
|
log.Print("Error listing mothballs: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
filename := f.Name()
|
||||||
|
filepath := m.path(filename)
|
||||||
|
if !strings.HasSuffix(filename, ".mb") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
categoryName := strings.TrimSuffix(filename, ".mb")
|
||||||
|
|
||||||
|
if _, ok := m.categories[categoryName]; !ok {
|
||||||
|
zfs, err := OpenZipfs(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("Error opening ", filepath, ": ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Print("New mothball: ", filename)
|
||||||
|
m.categories[categoryName] = zfs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mothballs) Run(updateInterval time.Duration) {
|
||||||
|
ticker := time.NewTicker(updateInterval)
|
||||||
|
m.update()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case when := <-ticker.C:
|
||||||
|
log.Print("Tick: ", when)
|
||||||
|
m.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,15 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"os"
|
|
||||||
"io/ioutil"
|
|
||||||
"bufio"
|
"bufio"
|
||||||
"time"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
||||||
|
@ -34,29 +33,19 @@ type StateExport struct {
|
||||||
// We use the filesystem for synchronization between threads.
|
// We use the filesystem for synchronization between threads.
|
||||||
// The only thing State methods need to know is the path to the state directory.
|
// The only thing State methods need to know is the path to the state directory.
|
||||||
type State struct {
|
type State struct {
|
||||||
StateDir string
|
Component
|
||||||
update chan bool
|
update chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewState(stateDir string) (*State) {
|
func NewState(baseDir string) *State {
|
||||||
return &State{
|
return &State{
|
||||||
StateDir: stateDir,
|
Component: Component{
|
||||||
update: make(chan bool, 10),
|
baseDir: baseDir,
|
||||||
|
},
|
||||||
|
update: make(chan bool, 10),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a cleaned up join of path parts relative to
|
|
||||||
func (s *State) path(parts ...string) string {
|
|
||||||
rel := filepath.Clean(filepath.Join(parts...))
|
|
||||||
parts = filepath.SplitList(rel)
|
|
||||||
for i, part := range parts {
|
|
||||||
part = strings.TrimLeft(part, "./\\:")
|
|
||||||
parts[i] = part
|
|
||||||
}
|
|
||||||
rel = filepath.Join(parts...)
|
|
||||||
return filepath.Join(s.StateDir, rel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check a few things to see if this state directory is "enabled".
|
// Check a few things to see if this state directory is "enabled".
|
||||||
func (s *State) Enabled() bool {
|
func (s *State) Enabled() bool {
|
||||||
if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) {
|
if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) {
|
||||||
|
@ -92,19 +81,19 @@ func (s *State) TeamName(teamId string) (string, error) {
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err)
|
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return teamName, nil
|
return teamName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write out team name. This can only be done once.
|
// Write out team name. This can only be done once.
|
||||||
func (s *State) SetTeamName(teamId string, teamName string) error {
|
func (s *State) SetTeamName(teamId string, teamName string) error {
|
||||||
teamFile := s.path("teams", teamId)
|
teamFile := s.path("teams", teamId)
|
||||||
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive | 0644)
|
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive|0644)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the current points log
|
// Retrieve the current points log
|
||||||
func (s *State) PointsLog() ([]*Award) {
|
func (s *State) PointsLog() []*Award {
|
||||||
pointsFile := s.path("points.log")
|
pointsFile := s.path("points.log")
|
||||||
f, err := os.Open(pointsFile)
|
f, err := os.Open(pointsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -112,7 +101,7 @@ func (s *State) PointsLog() ([]*Award) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
pointsLog := make([]*Award, 0, 200)
|
pointsLog := make([]*Award, 0, 200)
|
||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
@ -130,17 +119,17 @@ func (s *State) PointsLog() ([]*Award) {
|
||||||
// Return an exportable points log,
|
// Return an exportable points log,
|
||||||
// This anonymizes teamId with either an integer, or the string "self"
|
// This anonymizes teamId with either an integer, or the string "self"
|
||||||
// for the requesting teamId.
|
// for the requesting teamId.
|
||||||
func (s *State) Export(teamId string) (*StateExport) {
|
func (s *State) Export(teamId string) *StateExport {
|
||||||
teamName, _ := s.TeamName(teamId)
|
teamName, _ := s.TeamName(teamId)
|
||||||
|
|
||||||
pointsLog := s.PointsLog()
|
pointsLog := s.PointsLog()
|
||||||
|
|
||||||
export := StateExport{
|
export := StateExport{
|
||||||
PointsLog: make([]Award, len(pointsLog)),
|
PointsLog: make([]Award, len(pointsLog)),
|
||||||
Messages: make([]string, 0, 10),
|
Messages: make([]string, 0, 10),
|
||||||
TeamNames: map[string]string{"self": teamName},
|
TeamNames: map[string]string{"self": teamName},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in messages
|
// Read in messages
|
||||||
messagesFile := s.path("messages.txt")
|
messagesFile := s.path("messages.txt")
|
||||||
if f, err := os.Open(messagesFile); err != nil {
|
if f, err := os.Open(messagesFile); err != nil {
|
||||||
|
@ -156,7 +145,7 @@ func (s *State) Export(teamId string) (*StateExport) {
|
||||||
export.Messages = append(export.Messages, message)
|
export.Messages = append(export.Messages, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in points
|
// Read in points
|
||||||
exportIds := map[string]string{teamId: "self"}
|
exportIds := map[string]string{teamId: "self"}
|
||||||
for logno, award := range pointsLog {
|
for logno, award := range pointsLog {
|
||||||
|
@ -167,7 +156,7 @@ func (s *State) Export(teamId string) (*StateExport) {
|
||||||
exportId := strconv.Itoa(logno)
|
exportId := strconv.Itoa(logno)
|
||||||
exportAward.TeamId = exportId
|
exportAward.TeamId = exportId
|
||||||
exportIds[award.TeamId] = exportAward.TeamId
|
exportIds[award.TeamId] = exportAward.TeamId
|
||||||
|
|
||||||
name, err := s.TeamName(award.TeamId)
|
name, err := s.TeamName(award.TeamId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
|
name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
|
||||||
|
@ -176,7 +165,7 @@ func (s *State) Export(teamId string) (*StateExport) {
|
||||||
}
|
}
|
||||||
export.PointsLog[logno] = *exportAward
|
export.PointsLog[logno] = *exportAward
|
||||||
}
|
}
|
||||||
|
|
||||||
return &export
|
return &export
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,10 +258,9 @@ func (s *State) collectPoints() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *State) maybeInitialize() {
|
func (s *State) maybeInitialize() {
|
||||||
// Are we supposed to re-initialize?
|
// Are we supposed to re-initialize?
|
||||||
if _, err := os.Stat(s.path("initialized")); ! os.IsNotExist(err) {
|
if _, err := os.Stat(s.path("initialized")); !os.IsNotExist(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +306,7 @@ func (s *State) maybeInitialize() {
|
||||||
)
|
)
|
||||||
ioutil.WriteFile(
|
ioutil.WriteFile(
|
||||||
s.path("messages.txt"),
|
s.path("messages.txt"),
|
||||||
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now())),
|
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))),
|
||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
ioutil.WriteFile(
|
ioutil.WriteFile(
|
||||||
|
@ -334,23 +322,10 @@ func (s *State) Run(updateInterval time.Duration) {
|
||||||
if s.Enabled() {
|
if s.Enabled() {
|
||||||
s.collectPoints()
|
s.collectPoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-s.update:
|
case <-s.update:
|
||||||
case <-time.After(updateInterval):
|
case <-time.After(updateInterval):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
s := NewState("./state")
|
|
||||||
go s.Run(2 * time.Second)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(s.Export(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,23 +7,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Theme struct {
|
type Theme struct {
|
||||||
ThemeDir string
|
Component
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTheme(themeDir string) *Theme {
|
func NewTheme(baseDir string) *Theme {
|
||||||
return &Theme{
|
return &Theme{
|
||||||
ThemeDir: themeDir,
|
Component: Component{
|
||||||
|
baseDir: baseDir,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) path(parts ...string) string {
|
|
||||||
return MothPath(t.ThemeDir, parts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) {
|
func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
path := req.URL.Path
|
path := req.URL.Path
|
||||||
if strings.Contains(path, "/.") {
|
if strings.Contains(path, "/.") {
|
||||||
http.Error(w, "Invalid URL path", http.StatusBadRequest)
|
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"golang.org/x/net/webdav"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StubLockSystem struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *StubLockSystem) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) {
|
|
||||||
return nil, webdav.ErrConfirmationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *StubLockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) {
|
|
||||||
return "", webdav.ErrLocked
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *StubLockSystem) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) {
|
|
||||||
return webdav.LockDetails{}, webdav.ErrNoSuchLock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *StubLockSystem) Unlock(now time.Time, token string) error {
|
|
||||||
return webdav.ErrNoSuchLock
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type MothFS struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *MothFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
|
||||||
return os.ErrPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *MothFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
|
||||||
f, err := os.Open("hello.txt")
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *MothFS) RemoveAll(ctx context.Context, name string) error {
|
|
||||||
return os.ErrPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *MothFS) Rename(ctx context.Context, oldName, newName string) error {
|
|
||||||
return os.ErrPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *MothFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
|
||||||
info, err := os.Stat("hello.txt")
|
|
||||||
return info, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
//dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD")
|
|
||||||
httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
srv := &webdav.Handler{
|
|
||||||
FileSystem: new(MothFS),
|
|
||||||
LockSystem: new(StubLockSystem),
|
|
||||||
Logger: func(r *http.Request, err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("WEBDAV [%s]: %s, ERROR: %s\n", r.Method, r.URL, err)
|
|
||||||
} else {
|
|
||||||
log.Printf("WEBDAV [%s]: %s \n", r.Method, r.URL)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
http.Handle("/", srv)
|
|
||||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil {
|
|
||||||
log.Fatalf("Error with WebDAV server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -18,18 +18,23 @@ func seedJoin(parts ...string) string {
|
||||||
func usage() {
|
func usage() {
|
||||||
out := flag.CommandLine.Output()
|
out := flag.CommandLine.Output()
|
||||||
name := flag.CommandLine.Name()
|
name := flag.CommandLine.Name()
|
||||||
fmt.Fprintf(out, "Usage: %s [OPTIONS] CATEGORY [CATEGORY ...]\n", name)
|
fmt.Fprintf(out, "Usage: %s [OPTION]... CATEGORY [PUZZLE [FILENAME]]\n", name)
|
||||||
|
fmt.Fprintf(out, "\n")
|
||||||
|
fmt.Fprintf(out, "Transpile CATEGORY, or provide individual category components.\n")
|
||||||
|
fmt.Fprintf(out, "If PUZZLE is provided, only transpile the given puzzle.\n")
|
||||||
|
fmt.Fprintf(out, "If FILENAME is provided, output provided file.\n")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// XXX: We need a way to pass in "only run this one point value puzzle"
|
|
||||||
// XXX: Convert puzzle.py to standalone thingies
|
// XXX: Convert puzzle.py to standalone thingies
|
||||||
|
|
||||||
flag.Usage = usage
|
flag.Usage = usage
|
||||||
points := flag.Int("points", 0, "Transpile only this point value puzzle")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
|
points := flag.Int("points", 0, "Transpile only this point value puzzle")
|
||||||
|
mothball := flag.Bool("mothball", false, "Generate a mothball")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
baseSeedString := os.Getenv("MOTH_SEED")
|
baseSeedString := os.Getenv("MOTH_SEED")
|
||||||
|
|
||||||
jsenc := json.NewEncoder(os.Stdout)
|
jsenc := json.NewEncoder(os.Stdout)
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
package main
|
package main
|
||||||
|
|
Loading…
Reference in New Issue