From ba05cd52a61db42faa32721e894df01c7e74af46 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Thu, 15 Aug 2024 23:24:43 -0700 Subject: [PATCH] kennel POC --- adapters/files/files_adapter.go | 1 + adapters/files/filesystem/filesystem.go | 5 + api/kennel/kennel.go | 195 ++++++++++++++++++++++++ api/serve.go | 34 +++-- database/kennel.go | 25 ++- database/migrate.go | 1 + static/css/colors.css | 4 + static/css/styles.css | 101 ++++++++++++ static/css/table.css | 2 +- static/img/cat_spritesheets/default.gif | Bin 0 -> 6512 bytes templates/base.html | 2 + templates/dns.html | 2 +- templates/kennel.html | 81 ---------- templates/kennel_cats.html | 66 ++++++++ templates/kennel_enc.json | 1 + 15 files changed, 417 insertions(+), 103 deletions(-) create mode 100644 api/kennel/kennel.go create mode 100644 static/img/cat_spritesheets/default.gif delete mode 100644 templates/kennel.html create mode 100644 templates/kennel_cats.html create mode 100644 templates/kennel_enc.json diff --git a/adapters/files/files_adapter.go b/adapters/files/files_adapter.go index bf3ea5f..44853e1 100644 --- a/adapters/files/files_adapter.go +++ b/adapters/files/files_adapter.go @@ -4,5 +4,6 @@ import "io" type FilesAdapter interface { CreateFile(path string, content io.Reader) (string, error) + FileExists(path string) bool DeleteFile(path string) error } diff --git a/adapters/files/filesystem/filesystem.go b/adapters/files/filesystem/filesystem.go index 726a588..e7e671f 100644 --- a/adapters/files/filesystem/filesystem.go +++ b/adapters/files/filesystem/filesystem.go @@ -35,3 +35,8 @@ func (f *FilesystemAdapter) CreateFile(path string, content io.Reader) (string, func (f *FilesystemAdapter) DeleteFile(path string) error { return os.Remove(f.BasePath + path) } + +func (f *FilesystemAdapter) FileExists(path string) bool { + _, err := os.Stat(f.BasePath + path) + return err == nil +} diff --git a/api/kennel/kennel.go b/api/kennel/kennel.go new file mode 100644 index 0000000..60a898e --- /dev/null +++ b/api/kennel/kennel.go @@ -0,0 +1,195 @@ +package kennel + +import ( + "log" + "net/http" + "strconv" + "strings" + + "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" +) + +const MaxCatSize = 1024 * 100 // 60KB +const CatsPath = "cats/" +const CatsPrefix = "/uploads/cats/" +const DefaultCatSpritesheet = "/static/img/cat_spritesheets/default.gif" +const MaxUserCats = 15 + +func ListUserCatsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + userID := context.User.ID + + cats, err := database.GetUserKennelCats(context.DBConn, userID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + (*context.TemplateData)["Cats"] = cats + return success(context, req, resp) + } +} + +func CreateCatContinuation(fileAdapter files.FilesAdapter, maxUserCats int, maxCatSize int, catsPath string, catsPrefix string, defaultCatSpritesheet string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + formErrors := types.BannerMessages{ + Messages: []string{}, + } + + numCats, err := database.CountUserKennelCats(context.DBConn, context.User.ID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + if numCats >= maxUserCats { + formErrors.Messages = append(formErrors.Messages, "max cats reached for user") + } + + err = req.ParseMultipartForm(int64(maxCatSize)) + if err != nil { + formErrors.Messages = append(formErrors.Messages, "cat spritesheet too large") + } + + catID := utils.RandomId() + spritesheetPath := catsPrefix + catID + + if len(formErrors.Messages) == 0 { + file, _, err := req.FormFile("spritesheet") + if file != nil && err != nil { + formErrors.Messages = append(formErrors.Messages, "error uploading spritesheet") + } else if file != nil { + defer file.Close() + reader := http.MaxBytesReader(resp, file, int64(maxCatSize)) + defer reader.Close() + + _, err = fileAdapter.CreateFile(catsPath+catID, reader) + if err != nil { + log.Println(err) + formErrors.Messages = append(formErrors.Messages, "error saving spritesheet (is it too big?)") + } + } else if file == nil && err != nil { + spritesheetPath = defaultCatSpritesheet + } + } + + link := req.FormValue("link") + description := req.FormValue("description") + name := req.FormValue("name") + + cat := &database.KennelCat{ + ID: catID, + UserID: context.User.ID, + Name: name, + Link: link, + Description: description, + Spritesheet: spritesheetPath, + } + formErrors.Messages = append(formErrors.Messages, validateCat(cat)...) + if len(formErrors.Messages) == 0 { + _, err := database.SaveKennelCat(context.DBConn, cat) + if err != nil { + log.Println(err) + formErrors.Messages = append(formErrors.Messages, "failed to save cat") + } + } + + if len(formErrors.Messages) > 0 { + (*context.TemplateData)["Error"] = formErrors + (*context.TemplateData)["CatForm"] = cat + resp.WriteHeader(http.StatusBadRequest) + + return failure(context, req, resp) + } + + formSuccess := types.BannerMessages{ + Messages: []string{"cat added."}, + } + (*context.TemplateData)["Success"] = formSuccess + return success(context, req, resp) + } + } +} + +func RemoveCatContinuation(fileAdapter files.FilesAdapter, catsPath string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + catID := req.FormValue("id") + + cat, err := database.GetKennelCat(context.DBConn, catID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + if cat == nil || cat.UserID != context.User.ID { + resp.WriteHeader(http.StatusUnauthorized) + return failure(context, req, resp) + } + + err = database.DeleteKennelCat(context.DBConn, catID) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + err = fileAdapter.DeleteFile(catsPath + catID) + if err != nil && fileAdapter.FileExists(catsPath+catID) { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + return success(context, req, resp) + } + } +} + +func GetCatStateAtContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + atLong, err := strconv.ParseInt(req.FormValue("at"), 10, 64) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusBadRequest) + return failure(context, req, resp) + } + + cats, err := database.GetKennelStateAt(context.DBConn, atLong) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + (*context.TemplateData)["EncodedState"] = cats.EncodedState + return success(context, req, resp) + } +} + +func validateCat(cat *database.KennelCat) []string { + errors := []string{} + + if cat.Name == "" { + errors = append(errors, "name is required") + } + if cat.Link == "" { + errors = append(errors, "link is required") + } + if !strings.HasPrefix(cat.Link, "http://") && !strings.HasPrefix(cat.Link, "https://") { + errors = append(errors, "link must be a valid URL") + } + if cat.Description == "" { + errors = append(errors, "description is required") + } + if len(cat.Description) > 100 { + errors = append(errors, "description must be less than 100 characters") + } + + return errors +} diff --git a/api/serve.go b/api/serve.go index 2078cdc..2f08cf9 100644 --- a/api/serve.go +++ b/api/serve.go @@ -13,6 +13,7 @@ import ( "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/hcaptcha" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/kennel" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/keys" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/profiles" "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template" @@ -172,23 +173,28 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(hcaptcha.CaptchaVerificationContinuation, hcaptcha.CaptchaVerificationContinuation)(guestbook.SignGuestbookContinuation, FailurePassingContinuation)(guestbook.ListGuestbookContinuation, guestbook.ListGuestbookContinuation)(hcaptcha.CaptchaArgsContinuation, hcaptcha.CaptchaArgsContinuation)(template.TemplateContinuation("guestbook.html", true), template.TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) }) - /* - mux.HandleFunc("GET /kennel", func(w http.ResponseWriter, r *http.Request) { - requestContext := makeRequestContext() - LogRequestContinuation(requestContext, r, w)(kennel.GetKennelStateContinuation, kennel.GetKennelStateContinuation)(template.TemplateContinuation("kennel.json", true), template.TemplateContinuation("kennel.json", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) - }) + mux.HandleFunc("GET /kennel", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.GetCatStateAtContinuation, FailurePassingContinuation)(template.TemplateContinuation("kennel_enc.json", false), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) - mux.HandleFunc("GET /kennel/cats", func(w http.ResponseWriter, r *http.Request) { - requestContext := makeRequestContext() - LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.ListUserCats, auth.GoLoginContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) - }) + mux.HandleFunc("GET /kennel/cats", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + mux.HandleFunc("POST /kennel/cats", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + createCatContinuation := kennel.CreateCatContinuation(uploadAdapter, kennel.MaxUserCats, kennel.MaxCatSize, kennel.CatsPath, kennel.CatsPrefix, kennel.DefaultCatSpritesheet) + LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(createCatContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, kennel.ListUserCatsContinuation)(template.TemplateContinuation("kennel_cats.html", true), template.TemplateContinuation("kennel_cats.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) - mux.HandleFunc("POST /kennel", func(w http.ResponseWriter, r *http.Request) { - requestContext := makeRequestContext() - LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(hcaptcha.CaptchaVerificationContinuation, hcaptcha.CaptchaVerificationContinuation)(guestbook.SignGuestbookContinuation, FailurePassingContinuation)(guestbook.ListGuestbookContinuation, guestbook.ListGuestbookContinuation)(hcaptcha.CaptchaArgsContinuation, hcaptcha.CaptchaArgsContinuation)(template.TemplateContinuation("guestbook.html", true), template.TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) - }) - */ + mux.HandleFunc("POST /kennel/cats/delete", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + deleteCatContinuation := kennel.RemoveCatContinuation(uploadAdapter, kennel.CatsPath) + + LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(deleteCatContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, FailurePassingContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) mux.HandleFunc("GET /{template}", func(w http.ResponseWriter, r *http.Request) { requestContext := makeRequestContext() diff --git a/database/kennel.go b/database/kennel.go index 39d664c..7e9a557 100644 --- a/database/kennel.go +++ b/database/kennel.go @@ -10,6 +10,7 @@ import ( type KennelCat struct { ID string `json:"id"` UserID string `json:"user_id"` + Name string `json:"name"` Link string `json:"link"` Description string `json:"description"` Spritesheet string `json:"spritesheet"` @@ -45,7 +46,7 @@ func GetUserKennelCats(db *sql.DB, userID string) ([]KennelCat, error) { var cats []KennelCat for rows.Next() { var cat KennelCat - err := rows.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) + err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) if err != nil { return nil, err } @@ -62,7 +63,7 @@ func SaveKennelCat(db *sql.DB, cat *KennelCat) (*KennelCat, error) { cat.CreatedAt = time.Now() } - _, err := db.Exec("INSERT OR REPLACE INTO kennel_cat (id, user_id, link, description, spritesheet, created_at) VALUES (?, ?, ?, ?, ?, ?)", cat.ID, cat.UserID, cat.Link, cat.Description, cat.Spritesheet, cat.CreatedAt) + _, err := db.Exec("INSERT OR REPLACE INTO kennel_cat (id, user_id, name, link, description, spritesheet, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", cat.ID, cat.UserID, cat.Name, cat.Link, cat.Description, cat.Spritesheet, cat.CreatedAt) if err != nil { return nil, err @@ -75,7 +76,7 @@ func GetKennelCat(db *sql.DB, catID string) (*KennelCat, error) { row := db.QueryRow("SELECT * FROM kennel_cat WHERE id = ?", catID) var cat KennelCat - err := row.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) + err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) if err != nil { return nil, err } @@ -104,7 +105,7 @@ func GetKennel(dbConn *sql.DB) ([]KennelCat, error) { var cats []KennelCat for rows.Next() { var cat KennelCat - err := rows.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) + err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) if err != nil { return nil, err } @@ -114,10 +115,22 @@ func GetKennel(dbConn *sql.DB) ([]KennelCat, error) { return cats, nil } -func GetKennelState(dbConn *sql.DB) (*KennelState, error) { +func GetLatestKennelState(dbConn *sql.DB) (*KennelState, error) { log.Println("getting kennel state") - row := dbConn.QueryRow("SELECT * FROM kennel_state") + row := dbConn.QueryRow("SELECT * FROM kennel_state ORDER BY at DESC LIMIT 1") + var state KennelState + err := row.Scan(&state.At, &state.EncodedState) + if err != nil { + return nil, err + } + return &state, nil +} + +func GetKennelStateAt(dbConn *sql.DB, at int64) (*KennelState, error) { + log.Println("getting kennel state at", at) + + row := dbConn.QueryRow("SELECT * FROM kennel_state WHERE at = ?", at) var state KennelState err := row.Scan(&state.At, &state.EncodedState) if err != nil { diff --git a/database/migrate.go b/database/migrate.go index 363f022..14766b6 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -167,6 +167,7 @@ func MigrateKennel(dbConn *sql.DB) (*sql.DB, error) { _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS kennel_cat ( id TEXT PRIMARY KEY, + name TEXT NOT NULL, user_id INTEGER NOT NULL, link TEXT NOT NULL, description TEXT NOT NULL, diff --git a/static/css/colors.css b/static/css/colors.css index 46357d9..69b97db 100644 --- a/static/css/colors.css +++ b/static/css/colors.css @@ -7,6 +7,7 @@ --container-bg-light: #fff7f87a; --border-color-light: #692fcc; --error-color-light: #a83254; + --tr-color-light: #8bcefa; --background-color-dark: #333; --background-color-dark-2: #2c2c2c; @@ -16,6 +17,7 @@ --container-bg-dark: #424242ea; --border-color-dark: #956ade; --error-color-dark: #851736; + --tr-color-dark: #212a6a; } [data-theme="DARK"] { @@ -27,6 +29,7 @@ --border-color: var(--border-color-dark); --error-color: var(--error-color-dark); --confirm-color: var(--confirm-color-dark); + --tr-color: var(--tr-color-dark); } [data-theme="LIGHT"] { @@ -38,6 +41,7 @@ --border-color: var(--border-color-light); --error-color: var(--error-color-light); --confirm-color: var(--confirm-color-light); + --tr-color: var(--tr-color-light); } .error { diff --git a/static/css/styles.css b/static/css/styles.css index 24a76b9..9c82d6a 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -68,3 +68,104 @@ hr { .info:hover { opacity: 0.8; } +@import "/static/css/colors.css"; +@import "/static/css/blinky.css"; +@import "/static/css/table.css"; +@import "/static/css/form.css"; +@import "/static/css/guestbook.css"; +@import "/static/css/club.css"; + +@font-face { + font-family: "ComicSans"; + src: url("/static/font/comicsans.ttf"); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + color: var(--text-color); + font-family: "ComicSans", sans-serif; +} + +/* i just cannot get this to look good on firefox... */ +@supports not (-moz-appearance: none) { + * { + cursor: url("/static/img/cursor-2.png"), auto; + -webkit-animation: cursor 400ms infinite; + animation: cursor 400ms infinite; + } + + @-webkit-keyframes cursor { + 0% { + cursor: url("/static/img/cursor-2.png"), auto; + } + 50% { + cursor: url("/static/img/cursor-1.png"), auto; + } + 100% { + cursor: url("/static/img/cursor-2.png"), auto; + } + } + + @keyframes cursor { + 0% { + cursor: url("/static/img/cursor-2.png"), auto; + } + 50% { + cursor: url("/static/img/cursor-1.png"), auto; + } + 100% { + cursor: url("/static/img/cursor-2.png"), auto; + } + } +} + +body { + background-color: var(--background-color); + background-image: url("/static/img/stars.gif"); + min-height: 100vh; +} + +a { + color: var(--link-color); + text-decoration: none; + font-weight: bold; +} + +a:hover { + text-decoration: underline; +} + +.container { + max-width: 1600px; + margin: auto; + background-color: var(--container-bg); + padding: 1rem; +} + +hr { + border: 0; + border-top: 1px solid var(--text-color); + + margin: 20px 0; +} + +.blinkies { + display: flex; + justify-content: left; + flex-wrap: wrap; + max-width: 900px; + gap: 10px 10px; +} + +.info { + margin-bottom: 1rem; + max-width: 600px; + + transition: opacity 0.3s; +} + +.info:hover { + opacity: 0.8; +} diff --git a/static/css/table.css b/static/css/table.css index 75a961d..854f591 100644 --- a/static/css/table.css +++ b/static/css/table.css @@ -26,6 +26,6 @@ tbody tr { } tbody tr:hover { - background-color: #ff47daa0; + background-color: var(--tr-color); color: #2a2a2a; } diff --git a/static/img/cat_spritesheets/default.gif b/static/img/cat_spritesheets/default.gif new file mode 100644 index 0000000000000000000000000000000000000000..0d264d87d0b9106914df8d1475d80b978a87e609 GIT binary patch literal 6512 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 literal 0 HcmV?d00001 diff --git a/templates/base.html b/templates/base.html index 036a748..7486f45 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,6 +35,8 @@ | api keys. | + kennel. + | {{ .User.DisplayName }}. | logout. diff --git a/templates/dns.html b/templates/dns.html index 2f3f0a7..e04cbfa 100644 --- a/templates/dns.html +++ b/templates/dns.html @@ -37,7 +37,7 @@

note that the name must be a subdomain of {{ .User.Username }}


- - - type. - name. - content. - ttl. - internal. - created. - delete. - - {{ if (eq (len .DNSRecords) 0) }} - - no dns records found. - - {{ end }} - {{ range $record := .DNSRecords }} - - {{ $record.Type }} - {{ $record.Name }} - {{ $record.Content }} - {{ $record.TTL }} - {{ $record.Internal }} - {{ $record.CreatedAt }} - -
- - -
- - - {{ end }} - -
-
-

add dns records.

-

note that the name must be a subdomain of {{ .User.Username }}

-
- - - - - - - - - - -
-{{ end }} diff --git a/templates/kennel_cats.html b/templates/kennel_cats.html new file mode 100644 index 0000000..ce31738 --- /dev/null +++ b/templates/kennel_cats.html @@ -0,0 +1,66 @@ +{{ define "content" }} + + + + + + + + + + {{ if (eq (len .Cats) 0) }} + + + + {{ end }} + {{ range $cat := .Cats }} + + + + + + + + + {{ end }} +
name.link.description.spritesheet.created at.remove.
no cats found
{{ $cat.Name }}{{ $cat.Link }}{{ $cat.Description }}{{ $cat.CreatedAt }} +
+ + +
+
+
+
+

add cat.

+
+ + + + + + + + +
if not specified, will use the default. check it out for the format we expect. max 50KB.
+ + + +
+{{ end }} diff --git a/templates/kennel_enc.json b/templates/kennel_enc.json new file mode 100644 index 0000000..602dad1 --- /dev/null +++ b/templates/kennel_enc.json @@ -0,0 +1 @@ +{{ define "content" }}{{ .EncodedState }}{{ end }}