mirror of https://github.com/dirtbags/moth.git
Compare commits
110 Commits
f7945fcf3b
...
9071631353
Author | SHA1 | Date |
---|---|---|
Neale Pickett | 9071631353 | |
Neale Pickett | 43aec24d63 | |
Neale Pickett | b863955fdc | |
Neale Pickett | b293a9f0e9 | |
Neale Pickett | 34e51848be | |
Neale Pickett | 1ca2ec284f | |
Neale Pickett | 12979a55a3 | |
Neale Pickett | 3282ad22b0 | |
Neale Pickett | 5350cf73a0 | |
Neale Pickett | 768600e48e | |
Neale Pickett | bb4859e7a9 | |
Neale Pickett | d18de0fe8b | |
Neale Pickett | f49eb3ed46 | |
Neale Pickett | c72d13af32 | |
Neale Pickett | c0761933a9 | |
Neale Pickett | 4ce0dcf11a | |
Neale Pickett | d87be0bfcb | |
Neale Pickett | 13c17873d8 | |
Neale Pickett | 9ea39363b8 | |
Neale Pickett | 0831c4e3d5 | |
Neale Pickett | 175b7aaa1b | |
Neale Pickett | a82851fee3 | |
Neale Pickett | b135069851 | |
Neale Pickett | 18c5f044cc | |
Neale Pickett | 551afe04a5 | |
Neale Pickett | a896788cc5 | |
Neale Pickett | 8ff91e79ec | |
Neale Pickett | 47671b9a12 | |
Neale Pickett | 99d7245c49 | |
Neale Pickett | fcfa11b012 | |
Neale Pickett | a3d0f55160 | |
Neale Pickett | d2971ee740 | |
Neale Pickett | 67e8dda39d | |
Neale Pickett | c43ed9620b | |
dependabot[bot] | ded29f92c1 | |
Neale Pickett | 887e4b3eaf | |
John Donaldson | c3a7ee0d4f | |
John Donaldson | 7925547daf | |
John Donaldson | 5ba58664b6 | |
John Donaldson | 190657f2fa | |
John Donaldson | 73933447a9 | |
John Donaldson | 3bd1cdcc56 | |
John Donaldson | 5720961e85 | |
John Donaldson | 466de2d9c6 | |
John Donaldson | 92d904150a | |
Neale Pickett | 8e0f4561a5 | |
Neale Pickett | eb08700dd1 | |
Neale Pickett | a85df22479 | |
Neale Pickett | a387a06ae5 | |
Neale Pickett | cbe231ef12 | |
Neale Pickett | 243fdfd006 | |
Neale Pickett | bde4b2c86d | |
Neale Pickett | 85f5b96a40 | |
Neale Pickett | dfc31eb9f3 | |
Neale Pickett | be74961e94 | |
Neale Pickett | d014384b05 | |
Neale Pickett | 6d7fb9ebf5 | |
Neale Pickett | 5b6555cd9a | |
Neale Pickett | e5a3b26c93 | |
Neale Pickett | eea674b1a4 | |
Neale Pickett | b6eea388d9 | |
Neale Pickett | bb41697ba6 | |
Neale Pickett | 471ded7303 | |
Neale Pickett | 4bb6819319 | |
Neale Pickett | ace940ba12 | |
Neale Pickett | 2003b20cc4 | |
Neale Pickett | 40f8f71778 | |
Neale Pickett | a2ce3682ab | |
Neale Pickett | 127beca1fc | |
Neale Pickett | e15a505d7b | |
Neale Pickett | e1e9157841 | |
Neale Pickett | 6e5e2c3adf | |
Neale Pickett | 6f1f889be7 | |
Neale Pickett | ce037ebca3 | |
Neale Pickett | 459d774726 | |
Neale Pickett | e349a18861 | |
Neale Pickett | d51e4c2504 | |
Neale Pickett | 62b043354b | |
Neale Pickett | fd6d319218 | |
Neale Pickett | 781217d2ef | |
Neale Pickett | 2952c1b21c | |
Neale Pickett | 4934396936 | |
Neale Pickett | b5d4ab5c15 | |
Neale Pickett | e2e7d37300 | |
Neale Pickett | de1fdc0691 | |
Neale Pickett | 31fd9c2fab | |
Neale Pickett | c0092751b5 | |
Neale Pickett | 79f58ff83c | |
Neale Pickett | c743148eeb | |
Neale Pickett | 7a672d2fcf | |
Neale Pickett | 61e09eaa71 | |
Neale Pickett | 30dcda05e4 | |
Neale Pickett | e64e4dfa67 | |
Neale Pickett | 362ba11cf5 | |
Neale Pickett | e05bc90b3a | |
Neale Pickett | 54442ed1b9 | |
Neale Pickett | c477c1d2ea | |
Neale Pickett | 0d7a8fc935 | |
Neale Pickett | 16a02b93ab | |
Neale Pickett | ec9b2e2772 | |
Neale Pickett | 4dbeaac0bf | |
Neale Pickett | 5adf32f456 | |
Neale Pickett | a585afdd8d | |
Neale Pickett | d9299f5e59 | |
Neale Pickett | 41a0e6dffc | |
Neale Pickett | 454e643886 | |
Neale Pickett | 46fea903a6 | |
Neale Pickett | f68201ab53 | |
Neale Pickett | dd8ca81186 | |
Neale Pickett | d36c0da5bf |
|
@ -1,70 +0,0 @@
|
|||
name: Build/Test/Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v3
|
||||
- devel
|
||||
- main
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
test-mothd:
|
||||
name: Test mothd
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.13
|
||||
|
||||
- name: Retrieve code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Test
|
||||
run: go test ./...
|
||||
|
||||
publish:
|
||||
name: Publish container images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Retrieve code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Gitlab variables
|
||||
id: vars
|
||||
run: build/ci/gitlab-vars
|
||||
|
||||
- name: Login to GitHub Packages Docker Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.CR_PAT }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: neale
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
# Currently required, because buildx doesn't support auto-push from docker
|
||||
- name: Set up builder
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Build and push moth image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
target: moth
|
||||
file: build/package/Containerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
tags: |
|
||||
dirtbags/moth:${{ steps.vars.outputs.tag }}
|
||||
ghcr.io/dirtbags/moth:${{ steps.vars.outputs.tag }}
|
|
@ -0,0 +1,49 @@
|
|||
stages:
|
||||
- test
|
||||
- push
|
||||
|
||||
Run unit tests:
|
||||
stage: test
|
||||
image: &goimage golang:1.21
|
||||
only:
|
||||
refs:
|
||||
- main
|
||||
- tags
|
||||
- merge_requests
|
||||
script:
|
||||
- go test -coverprofile=coverage.txt -covermode=atomic -race ./...
|
||||
- go tool cover -html=coverage.txt -o coverage.html
|
||||
- go tool cover -func coverage.txt
|
||||
coverage: /\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage.html
|
||||
- coverage.txt
|
||||
Generage coverage XML:
|
||||
stage: test
|
||||
image: *goimage
|
||||
needs: ["Run unit tests"]
|
||||
script:
|
||||
- go get github.com/boumenot/gocover-cobertura
|
||||
- go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml
|
||||
only:
|
||||
refs:
|
||||
- main
|
||||
- tags
|
||||
- merge_requests
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
push:
|
||||
stage: push
|
||||
needs: ["Run unit tests"]
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||
script:
|
||||
- mkdir ~/.docker
|
||||
- echo "$DOCKER_AUTH_CONFIG" | tee ~/.docker/config.json | md5sum
|
||||
- sh build/ci/ci.sh publish
|
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -4,7 +4,60 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v4.2.1] - unreleased
|
||||
## [v4.6.0] - unreleased
|
||||
### Changed
|
||||
- Answer hashes are now the first 4 characters of the hex-encoded SHA1 digest
|
||||
- Reworked the built-in theme
|
||||
- [moth.mjs](theme/moth.mjs) is now the standard MOTH library for ECMAScript
|
||||
- Devel mode no longer accepts an empty team ID
|
||||
|
||||
## [v4.4.9] - 2022-05-12
|
||||
### Changed
|
||||
- Added a performance optimization for events with a large number of teams
|
||||
backed by NFS
|
||||
|
||||
## [v4.4.8] - 2022-05-10
|
||||
### Changed
|
||||
- You can now join with a team ID not appearing in `teamids.txt`,
|
||||
as long as it is registered (in the `teams/` directory)
|
||||
|
||||
## [v4.4.7] - 2022-05-10
|
||||
### Changed
|
||||
- Initializing an instance now truncates `events.csv`
|
||||
|
||||
## [v4.4.6] - 2021-10-26
|
||||
### Added
|
||||
- State is now cached in memory, in an attempt to reduce filesystem metadata operations,
|
||||
which kill NFS.
|
||||
|
||||
## [v4.4.5] - 2021-10-26
|
||||
### Added
|
||||
- Images deploying to docker hub too. We're now at capacity for our Docker Hub team.
|
||||
|
||||
## [v4.4.4] - 2021-10-20
|
||||
### Changed
|
||||
- Trying to get CI push of built images. I expect this to fail, too. But in a way that can help me debug the issue.
|
||||
|
||||
## [v4.3.3] - 2021-10-20
|
||||
### Fixed
|
||||
- Points awarded while scoring is paused are now correctly sorted (#168)
|
||||
- Writing a new mothball with the same name is now detected and the new mothball loaded (#172)
|
||||
- Regression test for issue where URL path leading directories were ignored (#144)
|
||||
- A few other very minor bugs were closed when I couldn't reproduce them or decided they weren't actually bugs.
|
||||
|
||||
### Changed
|
||||
- Many error messages were changed to start with a lower-case letter,
|
||||
in order to satisfy a new linter check.
|
||||
- CI/CD moved to our Cyber Fire Gitlab instance
|
||||
- I attempted to have the build thingy automatically build moth:v4 and moth:v4.3 and moth:v4.3.3 images,
|
||||
but I can't test it without tagging a release.
|
||||
So v4.3.4 might come out very soon after this ;)
|
||||
|
||||
## [v4.2.2] - 2021-09-30
|
||||
### Added
|
||||
- `debug.notes` front matter field
|
||||
|
||||
## [v4.2.1] - 2021-04-13
|
||||
### Fixed
|
||||
- Transpiled KSAs no longer dropped
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
Dirtbags Monarch Of The Hill Server
|
||||
=====================
|
||||
|
||||
![Build badge](https://github.com/dirtbags/moth/workflows/Build/Test/Push/badge.svg)
|
||||
![Go report card](https://goreportcard.com/badge/github.com/dirtbags/moth)
|
||||
[![Go report card](https://goreportcard.com/badge/github.com/dirtbags/moth)](https://goreportcard.com/report/github.com/dirtbags/moth)
|
||||
|
||||
Monarch Of The Hill (MOTH) is a puzzle server.
|
||||
We (the authors) have used it for instructional and contest events called
|
||||
|
@ -33,7 +32,7 @@ You can read more about why we made these decisions in [philosophy](docs/philoso
|
|||
Run in demonstration mode
|
||||
===========
|
||||
|
||||
docker run --rm -it -p 8080:8080 dirtbags/moth-devel
|
||||
docker run --rm -it -p 8080:8080 ghcr.io/dirtbags/moth-devel
|
||||
|
||||
Then open http://localhost:8080/ and check out the example puzzles.
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
images="ghcr.io/dirtbags/moth dirtbags/moth"
|
||||
|
||||
ACTION=$1
|
||||
if [ -z "$ACTION" ]; then
|
||||
echo "Usage: $0 ACTION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log () {
|
||||
printf "=== %s\n" "$*" 1>&2
|
||||
}
|
||||
|
||||
fail () {
|
||||
printf "\033[31;1m=== FAIL: %s\033[0m\n" "$*" 1>&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run () {
|
||||
printf "\033[32m\$\033[0m %s\n" "$*" 1>&2
|
||||
"$@"
|
||||
}
|
||||
|
||||
tags () {
|
||||
pfx=$1
|
||||
for base in $images; do
|
||||
echo $pfx $base:${CI_COMMIT_REF_NAME}
|
||||
echo $pfx $base:${CI_COMMIT_REF_NAME%.*}
|
||||
echo $pfx $base:${CI_COMMIT_REF_NAME%.*.*}
|
||||
done | uniq
|
||||
}
|
||||
|
||||
case $ACTION in
|
||||
publish)
|
||||
run docker build \
|
||||
--file build/package/Containerfile \
|
||||
$(tags --tag) \
|
||||
.
|
||||
tags | while read image; do
|
||||
run docker push $image
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "Unknown action: $1" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -7,7 +7,7 @@ COPY example-puzzles /target/puzzles/
|
|||
COPY LICENSE.md /target/
|
||||
RUN mkdir -p /target/state
|
||||
WORKDIR /src/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go install -i -a -ldflags '-extldflags "-static"' ./...
|
||||
RUN CGO_ENABLED=0 GOOS=linux go install -a -ldflags '-extldflags "-static"' ./...
|
||||
# I can't use /target/bin: doing so would cause the devel server to overwrite Ubuntu's /bin
|
||||
|
||||
##########
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/jsend"
|
||||
"github.com/dirtbags/moth/v4/pkg/jsend"
|
||||
)
|
||||
|
||||
// HTTPServer is a MOTH HTTP server
|
||||
|
@ -44,9 +44,8 @@ func (h *HTTPServer) HandleMothFunc(
|
|||
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
|
||||
) {
|
||||
handler := func(w http.ResponseWriter, req *http.Request) {
|
||||
participantID := req.FormValue("pid")
|
||||
teamID := req.FormValue("id")
|
||||
mh := h.server.NewHandler(participantID, teamID)
|
||||
mh := h.server.NewHandler(teamID)
|
||||
mothHandler(mh, w, req)
|
||||
}
|
||||
h.HandleFunc(h.base+pattern, handler)
|
||||
|
@ -117,11 +116,11 @@ func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWrite
|
|||
}
|
||||
|
||||
if err := mh.Register(teamName); err == ErrAlreadyRegistered {
|
||||
jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered")
|
||||
jsend.Sendf(w, jsend.Success, "already registered", "team ID has already been registered")
|
||||
} else if err != nil {
|
||||
jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
|
||||
} else {
|
||||
jsend.Sendf(w, jsend.Success, "registered", "Team ID registered")
|
||||
jsend.Sendf(w, jsend.Success, "registered", "team ID registered")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,21 +7,15 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const TestParticipantID = "shipox"
|
||||
|
||||
func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest.ResponseRecorder {
|
||||
vals := url.Values{}
|
||||
vals.Set("pid", TestParticipantID)
|
||||
vals.Set("id", TestTeamID)
|
||||
if args != nil {
|
||||
for k, v := range args {
|
||||
vals.Set(k, v)
|
||||
}
|
||||
for k, v := range args {
|
||||
vals.Set(k, v)
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
@ -35,7 +29,8 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest
|
|||
}
|
||||
|
||||
func TestHttpd(t *testing.T) {
|
||||
hs := NewHTTPServer("/", NewTestServer())
|
||||
server := NewTestServer()
|
||||
hs := NewHTTPServer("/", server.MothServer)
|
||||
|
||||
if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
|
@ -56,22 +51,24 @@ func TestHttpd(t *testing.T) {
|
|||
|
||||
if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not registered","description":"Team ID not found in list of valid Team IDs"}}` {
|
||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not registered","description":"team ID not found in list of valid team IDs"}}` {
|
||||
t.Error("Register bad team ID failed")
|
||||
}
|
||||
|
||||
if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
} else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"Team ID registered"}}` {
|
||||
} else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"team ID registered"}}` {
|
||||
t.Error("Register failed")
|
||||
}
|
||||
|
||||
if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
} else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` {
|
||||
} else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"team ID has already been registered"}}` {
|
||||
t.Error("Register failed", r.Body.String())
|
||||
}
|
||||
|
||||
server.refresh()
|
||||
|
||||
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` {
|
||||
|
@ -102,7 +99,7 @@ func TestHttpd(t *testing.T) {
|
|||
|
||||
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Incorrect answer"}}` {
|
||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"incorrect answer"}}` {
|
||||
t.Error("Unexpected body", r.Body.String())
|
||||
}
|
||||
|
||||
|
@ -112,7 +109,7 @@ func TestHttpd(t *testing.T) {
|
|||
t.Error("Unexpected body", r.Body.String())
|
||||
}
|
||||
|
||||
time.Sleep(TestMaintenanceInterval)
|
||||
server.refresh()
|
||||
|
||||
if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
|
@ -124,14 +121,14 @@ func TestHttpd(t *testing.T) {
|
|||
} else if err := json.Unmarshal(r.Body.Bytes(), &state); err != nil {
|
||||
t.Error(err)
|
||||
} else if len(state.PointsLog) != 1 {
|
||||
t.Error("Points log wrong length")
|
||||
t.Errorf("Points log wrong length. Wanted 1, got %v (length %d)", state.PointsLog, len(state.PointsLog))
|
||||
} else if len(state.Puzzles["pategory"]) != 2 {
|
||||
t.Error("Didn't unlock next puzzle")
|
||||
}
|
||||
|
||||
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 {
|
||||
t.Error(r.Result())
|
||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Error awarding points: Points already awarded to this team in this category"}}` {
|
||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"points already awarded to this team in this category"}}` {
|
||||
t.Error("Unexpected body", r.Body.String())
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +137,7 @@ func TestDevelMemHttpd(t *testing.T) {
|
|||
srv := NewTestServer()
|
||||
|
||||
{
|
||||
hs := NewHTTPServer("/", srv)
|
||||
hs := NewHTTPServer("/", srv.MothServer)
|
||||
|
||||
if r := hs.TestRequest("/mothballer/pategory.md", nil); r.Result().StatusCode != 404 {
|
||||
t.Error("Should have gotten a 404 for mothballer in prod mode")
|
||||
|
@ -149,7 +146,7 @@ func TestDevelMemHttpd(t *testing.T) {
|
|||
|
||||
{
|
||||
srv.Config.Devel = true
|
||||
hs := NewHTTPServer("/", srv)
|
||||
hs := NewHTTPServer("/", srv.MothServer)
|
||||
|
||||
if r := hs.TestRequest("/mothballer/pategory.md", nil); r.Result().StatusCode != 500 {
|
||||
t.Log(r.Body.String())
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestIssue156(t *testing.T) {
|
||||
puzzles := NewTestMothballs()
|
||||
state := NewTestState()
|
||||
theme := NewTestTheme()
|
||||
server := NewMothServer(Configuration{}, theme, state, puzzles)
|
||||
|
||||
afero.WriteFile(state, "teams/bloop", []byte("bloop: the team"), 0644)
|
||||
state.refresh()
|
||||
|
||||
handler := server.NewHandler("bloop")
|
||||
es := handler.ExportState()
|
||||
if _, ok := es.TeamNames["self"]; !ok {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err := handler.Register("bloop: the other team")
|
||||
if err != ErrAlreadyRegistered {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import (
|
|||
type zipCategory struct {
|
||||
afero.Fs
|
||||
io.Closer
|
||||
mtime time.Time
|
||||
}
|
||||
|
||||
// Mothballs provides a collection of active mothball files (puzzle categories)
|
||||
|
@ -48,7 +49,7 @@ func (m *Mothballs) getCat(cat string) (zipCategory, bool) {
|
|||
func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
|
||||
zc, ok := m.getCat(cat)
|
||||
if !ok {
|
||||
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
|
||||
return nil, time.Time{}, fmt.Errorf("no such category: %s", cat)
|
||||
}
|
||||
|
||||
f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename))
|
||||
|
@ -91,12 +92,12 @@ func (m *Mothballs) Inventory() []Category {
|
|||
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
|
||||
zfs, ok := m.getCat(cat)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("No such category: %s", cat)
|
||||
return false, fmt.Errorf("no such category: %s", cat)
|
||||
}
|
||||
|
||||
af, err := zfs.Open("answers.txt")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("No answers.txt file")
|
||||
return false, fmt.Errorf("no answers.txt file")
|
||||
}
|
||||
defer af.Close()
|
||||
|
||||
|
@ -132,7 +133,18 @@ func (m *Mothballs) refresh() {
|
|||
categoryName := strings.TrimSuffix(filename, ".mb")
|
||||
found[categoryName] = true
|
||||
|
||||
if _, ok := m.categories[categoryName]; !ok {
|
||||
reopen := false
|
||||
if existingMothball, ok := m.categories[categoryName]; !ok {
|
||||
reopen = true
|
||||
} else if si, err := m.Fs.Stat(filename); err != nil {
|
||||
log.Println(err)
|
||||
} else if si.ModTime().After(existingMothball.mtime) {
|
||||
existingMothball.Close()
|
||||
delete(m.categories, categoryName)
|
||||
reopen = true
|
||||
}
|
||||
|
||||
if reopen {
|
||||
f, err := m.Fs.Open(filename)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
@ -156,6 +168,7 @@ func (m *Mothballs) refresh() {
|
|||
m.categories[categoryName] = zipCategory{
|
||||
Fs: zipfs.New(zrc),
|
||||
Closer: f,
|
||||
mtime: fi.ModTime(),
|
||||
}
|
||||
|
||||
log.Println("Adding category:", categoryName)
|
||||
|
@ -174,7 +187,7 @@ func (m *Mothballs) refresh() {
|
|||
|
||||
// Mothball just returns an error
|
||||
func (m *Mothballs) Mothball(cat string, w io.Writer) error {
|
||||
return fmt.Errorf("Refusing to repackage a compiled mothball")
|
||||
return fmt.Errorf("refusing to repackage a compiled mothball")
|
||||
}
|
||||
|
||||
// Maintain performs housekeeping for Mothballs.
|
||||
|
|
|
@ -3,25 +3,27 @@ package main
|
|||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var testFiles = []struct {
|
||||
type testFileContents struct {
|
||||
Name, Body string
|
||||
}{
|
||||
}
|
||||
|
||||
var testFiles = []testFileContents{
|
||||
{"puzzles.txt", "1\n3\n2\n"},
|
||||
{"answers.txt", "1 answer123\n1 answer456\n2 wat\n"},
|
||||
{"1/puzzle.json", `{"name": "moo"}`},
|
||||
{"1/moo.txt", `moo`},
|
||||
{"2/puzzle.json", `{}`},
|
||||
{"2/moo.txt", `moo`},
|
||||
{"3/puzzle.json", `{}`},
|
||||
{"3/moo.txt", `moo`},
|
||||
}
|
||||
|
||||
func (m *Mothballs) createMothball(cat string) {
|
||||
func (m *Mothballs) createMothballWithFiles(cat string, contents []testFileContents) {
|
||||
f, _ := m.Create(fmt.Sprintf("%s.mb", cat))
|
||||
defer f.Close()
|
||||
|
||||
|
@ -32,6 +34,19 @@ func (m *Mothballs) createMothball(cat string) {
|
|||
of, _ := w.Create(file.Name)
|
||||
of.Write([]byte(file.Body))
|
||||
}
|
||||
for _, file := range contents {
|
||||
of, _ := w.Create(file.Name)
|
||||
of.Write([]byte(file.Body))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mothballs) createMothball(cat string) {
|
||||
m.createMothballWithFiles(
|
||||
cat,
|
||||
[]testFileContents{
|
||||
{"1/moo.txt", "moo"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func NewTestMothballs() *Mothballs {
|
||||
|
@ -92,10 +107,27 @@ func TestMothballs(t *testing.T) {
|
|||
}
|
||||
if ok, err := m.CheckAnswer("nealegory", 1, "moo"); ok {
|
||||
t.Error("Checking answer in non-existent category should fail")
|
||||
} else if err.Error() != "No such category: nealegory" {
|
||||
} else if err.Error() != "no such category: nealegory" {
|
||||
t.Error("Wrong error message")
|
||||
}
|
||||
|
||||
goofyText := "bozonics"
|
||||
//time.Sleep(1 * time.Second) // I don't love this, but we need the mtime to increase, and it's only accurate to 1s
|
||||
m.createMothballWithFiles(
|
||||
"pategory",
|
||||
[]testFileContents{
|
||||
{"1/moo.txt", goofyText},
|
||||
},
|
||||
)
|
||||
m.refresh()
|
||||
if f, _, err := m.Open("pategory", 1, "moo.txt"); err != nil {
|
||||
t.Error("pategory/1/moo.txt", err)
|
||||
} else if contents, err := ioutil.ReadAll(f); err != nil {
|
||||
t.Error("read all pategory/1/moo.txt", err)
|
||||
} else if string(contents) != goofyText {
|
||||
t.Error("read all replacement pategory/1/moo.txt contents wrong, got", string(contents))
|
||||
}
|
||||
|
||||
m.createMothball("test2")
|
||||
m.Fs.Remove("pategory.mb")
|
||||
m.refresh()
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/transpile"
|
||||
"github.com/dirtbags/moth/v4/pkg/transpile"
|
||||
)
|
||||
|
||||
// ProviderCommand specifies a command to run for the puzzle API
|
||||
|
@ -125,7 +125,7 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo
|
|||
|
||||
// Mothball just returns an error
|
||||
func (pc ProviderCommand) Mothball(cat string) (*bytes.Reader, error) {
|
||||
return nil, fmt.Errorf("Can't package a command-generated category")
|
||||
return nil, fmt.Errorf("can't package a command-generated category")
|
||||
}
|
||||
|
||||
// Maintain does nothing: a command puzzle ProviderCommand has no housekeeping
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/award"
|
||||
"github.com/dirtbags/moth/v4/pkg/award"
|
||||
)
|
||||
|
||||
// Category represents a puzzle category.
|
||||
|
@ -58,7 +58,7 @@ type StateProvider interface {
|
|||
TeamName(teamID string) (string, error)
|
||||
SetTeamName(teamID, teamName string) error
|
||||
AwardPoints(teamID string, cat string, points int) error
|
||||
LogEvent(event, participantID, teamID, cat string, points int, extra ...string)
|
||||
LogEvent(event, teamID, cat string, points int, extra ...string)
|
||||
Maintainer
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,9 @@ type Maintainer interface {
|
|||
// It will only be called once, when execution begins.
|
||||
// It's okay to just exit if there's no maintenance to be done.
|
||||
Maintain(updateInterval time.Duration)
|
||||
|
||||
// refresh is a shortcut used internally for testing
|
||||
refresh()
|
||||
}
|
||||
|
||||
// MothServer gathers together the providers that make up a MOTH server.
|
||||
|
@ -89,19 +92,17 @@ func NewMothServer(config Configuration, theme ThemeProvider, state StateProvide
|
|||
}
|
||||
|
||||
// NewHandler returns a new http.RequestHandler for the provided teamID.
|
||||
func (s *MothServer) NewHandler(participantID, teamID string) MothRequestHandler {
|
||||
func (s *MothServer) NewHandler(teamID string) MothRequestHandler {
|
||||
return MothRequestHandler{
|
||||
MothServer: s,
|
||||
participantID: participantID,
|
||||
teamID: teamID,
|
||||
MothServer: s,
|
||||
teamID: teamID,
|
||||
}
|
||||
}
|
||||
|
||||
// MothRequestHandler provides http.RequestHandler for a MothServer.
|
||||
type MothRequestHandler struct {
|
||||
*MothServer
|
||||
participantID string
|
||||
teamID string
|
||||
teamID string
|
||||
}
|
||||
|
||||
// PuzzlesOpen opens a file associated with a puzzle.
|
||||
|
@ -115,7 +116,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, time.Time{}, fmt.Errorf("Puzzle does not exist or is locked")
|
||||
return nil, time.Time{}, fmt.Errorf("puzzle does not exist or is locked")
|
||||
}
|
||||
|
||||
// Try every provider until someone doesn't return an error
|
||||
|
@ -128,7 +129,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
|
|||
|
||||
// Log puzzle.json loads
|
||||
if path == "puzzle.json" {
|
||||
mh.State.LogEvent("load", mh.participantID, mh.teamID, cat, points)
|
||||
mh.State.LogEvent("load", mh.teamID, cat, points)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -145,17 +146,17 @@ func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string)
|
|||
}
|
||||
}
|
||||
if !correct {
|
||||
mh.State.LogEvent("wrong", mh.participantID, mh.teamID, cat, points)
|
||||
return fmt.Errorf("Incorrect answer")
|
||||
mh.State.LogEvent("wrong", mh.teamID, cat, points)
|
||||
return fmt.Errorf("incorrect answer")
|
||||
}
|
||||
|
||||
mh.State.LogEvent("correct", mh.participantID, mh.teamID, cat, points)
|
||||
mh.State.LogEvent("correct", mh.teamID, cat, points)
|
||||
|
||||
if _, err := mh.State.TeamName(mh.teamID); err != nil {
|
||||
return fmt.Errorf("Invalid team ID")
|
||||
return fmt.Errorf("invalid team ID")
|
||||
}
|
||||
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
|
||||
return fmt.Errorf("Error awarding points: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -168,11 +169,10 @@ func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time,
|
|||
|
||||
// Register associates a team name with a team ID.
|
||||
func (mh *MothRequestHandler) Register(teamName string) error {
|
||||
// BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success
|
||||
if teamName == "" {
|
||||
return fmt.Errorf("Empty team name")
|
||||
return fmt.Errorf("empty team name")
|
||||
}
|
||||
mh.State.LogEvent("register", mh.participantID, mh.teamID, "", 0)
|
||||
mh.State.LogEvent("register", mh.teamID, "", 0)
|
||||
return mh.State.SetTeamName(mh.teamID, teamName)
|
||||
}
|
||||
|
||||
|
@ -184,12 +184,15 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
|
|||
return mh.exportStateIfRegistered(false)
|
||||
}
|
||||
|
||||
func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExport {
|
||||
// Export state, replacing the team ID with "self" if the team is registered.
|
||||
//
|
||||
// If forceRegistered is true, go ahead and export it anyway
|
||||
func (mh *MothRequestHandler) exportStateIfRegistered(forceRegistered bool) *StateExport {
|
||||
export := StateExport{}
|
||||
export.Config = mh.Config
|
||||
|
||||
teamName, err := mh.State.TeamName(mh.teamID)
|
||||
registered := override || mh.Config.Devel || (err == nil)
|
||||
registered := forceRegistered || mh.Config.Devel || (err == nil)
|
||||
|
||||
export.Messages = mh.State.Messages()
|
||||
export.TeamNames = make(map[string]string)
|
||||
|
@ -254,7 +257,7 @@ func (mh *MothRequestHandler) Mothball(cat string, w io.Writer) error {
|
|||
var err error
|
||||
|
||||
if !mh.Config.Devel {
|
||||
return fmt.Errorf("Cannot mothball in production mode")
|
||||
return fmt.Errorf("cannot mothball in production mode")
|
||||
}
|
||||
for _, provider := range mh.PuzzleProviders {
|
||||
if err = provider.Mothball(cat, w); err == nil {
|
||||
|
|
|
@ -3,34 +3,46 @@ package main
|
|||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const TestMaintenanceInterval = time.Millisecond * 1
|
||||
const TestTeamID = "teamID"
|
||||
|
||||
func NewTestServer() *MothServer {
|
||||
type TestServer struct {
|
||||
*MothServer
|
||||
}
|
||||
|
||||
// NewTestServer creates a new MothServer with NewTestMothballs and some initial state.
|
||||
//
|
||||
// See function definition for details.
|
||||
func NewTestServer() TestServer {
|
||||
puzzles := NewTestMothballs()
|
||||
go puzzles.Maintain(TestMaintenanceInterval)
|
||||
puzzles.refresh()
|
||||
|
||||
state := NewTestState()
|
||||
afero.WriteFile(state, "teamids.txt", []byte("teamID\n"), 0644)
|
||||
afero.WriteFile(state, "messages.html", []byte("messages.html"), 0644)
|
||||
go state.Maintain(TestMaintenanceInterval)
|
||||
state.refresh()
|
||||
|
||||
theme := NewTestTheme()
|
||||
afero.WriteFile(theme.Fs, "/index.html", []byte("index.html"), 0644)
|
||||
go theme.Maintain(TestMaintenanceInterval)
|
||||
|
||||
return NewMothServer(Configuration{}, theme, state, puzzles)
|
||||
return TestServer{NewMothServer(Configuration{}, theme, state, puzzles)}
|
||||
}
|
||||
|
||||
func (ts TestServer) refresh() {
|
||||
ts.State.(*State).refresh()
|
||||
for _, pp := range ts.PuzzleProviders {
|
||||
pp.(*Mothballs).refresh()
|
||||
}
|
||||
ts.Theme.(*Theme).refresh()
|
||||
}
|
||||
|
||||
func TestDevelServer(t *testing.T) {
|
||||
server := NewTestServer()
|
||||
server.Config.Devel = true
|
||||
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
|
||||
anonHandler := server.NewHandler("badTeamId")
|
||||
|
||||
{
|
||||
es := anonHandler.ExportState()
|
||||
|
@ -45,12 +57,11 @@ func TestDevelServer(t *testing.T) {
|
|||
|
||||
func TestProdServer(t *testing.T) {
|
||||
teamName := "OurTeam"
|
||||
participantID := "participantID"
|
||||
teamID := TestTeamID
|
||||
|
||||
server := NewTestServer()
|
||||
handler := server.NewHandler(participantID, teamID)
|
||||
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
|
||||
handler := server.NewHandler(teamID)
|
||||
anonHandler := server.NewHandler("badTeamId")
|
||||
|
||||
{
|
||||
es := handler.ExportState()
|
||||
|
@ -80,13 +91,15 @@ func TestProdServer(t *testing.T) {
|
|||
t.Error("index.html wrong contents", contents)
|
||||
}
|
||||
|
||||
server.refresh()
|
||||
|
||||
{
|
||||
es := handler.ExportState()
|
||||
if es.Config.Devel {
|
||||
t.Error("Marked as development server", es.Config)
|
||||
}
|
||||
if len(es.Puzzles) != 1 {
|
||||
t.Error("Puzzle categories wrong length")
|
||||
t.Error("Puzzle categories wrong length", len(es.Puzzles))
|
||||
}
|
||||
if es.Messages != "messages.html" {
|
||||
t.Error("Messages has wrong contents")
|
||||
|
@ -131,7 +144,7 @@ func TestProdServer(t *testing.T) {
|
|||
t.Error("Right answer marked wrong", err)
|
||||
}
|
||||
|
||||
time.Sleep(TestMaintenanceInterval)
|
||||
server.refresh()
|
||||
|
||||
{
|
||||
es := handler.ExportState()
|
||||
|
@ -160,7 +173,7 @@ func TestProdServer(t *testing.T) {
|
|||
t.Error("Right answer marked wrong:", err)
|
||||
}
|
||||
|
||||
time.Sleep(TestMaintenanceInterval)
|
||||
server.refresh()
|
||||
|
||||
{
|
||||
es := anonHandler.ExportState()
|
||||
|
@ -168,9 +181,8 @@ func TestProdServer(t *testing.T) {
|
|||
t.Error("Anonymous TeamNames is wrong:", es.TeamNames)
|
||||
}
|
||||
if len(es.PointsLog) != 2 {
|
||||
t.Error("Points log wrong length")
|
||||
}
|
||||
if es.PointsLog[1].TeamID != "0" {
|
||||
t.Errorf("Points log wrong length: got %d, wanted 2", len(es.PointsLog))
|
||||
} else if es.PointsLog[1].TeamID != "0" {
|
||||
t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dirtbags/moth/pkg/award"
|
||||
"github.com/dirtbags/moth/v4/pkg/award"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
|
@ -27,7 +28,7 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy="
|
|||
const RFC3339Space = "2006-01-02 15:04:05Z07:00"
|
||||
|
||||
// ErrAlreadyRegistered means a team cannot be registered because it was registered previously.
|
||||
var ErrAlreadyRegistered = errors.New("Team ID has already been registered")
|
||||
var ErrAlreadyRegistered = errors.New("team ID has already been registered")
|
||||
|
||||
// State defines the current state of a MOTH instance.
|
||||
// We use the filesystem for synchronization between threads.
|
||||
|
@ -38,10 +39,18 @@ type State struct {
|
|||
// Enabled tracks whether the current State system is processing updates
|
||||
Enabled bool
|
||||
|
||||
enabledWhy string
|
||||
refreshNow chan bool
|
||||
eventStream chan []string
|
||||
eventWriter *csv.Writer
|
||||
eventWriterFile afero.File
|
||||
|
||||
// Caches, so we're not hammering NFS with metadata operations
|
||||
teamNamesLastChange time.Time
|
||||
teamNames map[string]string
|
||||
pointsLog award.List
|
||||
messages string
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewState returns a new State struct backed by the given Fs
|
||||
|
@ -51,6 +60,8 @@ func NewState(fs afero.Fs) *State {
|
|||
Enabled: true,
|
||||
refreshNow: make(chan bool, 5),
|
||||
eventStream: make(chan []string, 80),
|
||||
|
||||
teamNames: make(map[string]string),
|
||||
}
|
||||
if err := s.reopenEventLog(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -61,11 +72,10 @@ func NewState(fs afero.Fs) *State {
|
|||
// updateEnabled checks a few things to see if this state directory is "enabled".
|
||||
func (s *State) updateEnabled() {
|
||||
nextEnabled := true
|
||||
why := "`state/enabled` present, `state/hours.txt` missing"
|
||||
why := "state/hours.txt has no timestamps before now"
|
||||
|
||||
if untilFile, err := s.Open("hours.txt"); err == nil {
|
||||
defer untilFile.Close()
|
||||
why = "`state/hours.txt` present"
|
||||
|
||||
scanner := bufio.NewScanner(untilFile)
|
||||
for scanner.Scan() {
|
||||
|
@ -85,59 +95,64 @@ func (s *State) updateEnabled() {
|
|||
case '#':
|
||||
continue
|
||||
default:
|
||||
log.Println("Misformatted line in hours.txt file")
|
||||
log.Println("state/hours.txt has bad line:", line)
|
||||
}
|
||||
line, _, _ = strings.Cut(line, "#") // Remove inline comments
|
||||
line = strings.TrimSpace(line)
|
||||
until, err := time.Parse(time.RFC3339, line)
|
||||
if err != nil {
|
||||
until, err = time.Parse(RFC3339Space, line)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("Suspended: Unparseable until date:", line)
|
||||
until := time.Time{}
|
||||
if len(line) == 0 {
|
||||
// Let it stay as zero time, so it's always before now
|
||||
} else if until, err = time.Parse(time.RFC3339, line); err == nil {
|
||||
// Great, it was RFC 3339
|
||||
} else if until, err = time.Parse(RFC3339Space, line); err == nil {
|
||||
// Great, it was RFC 3339 with a space instead of a 'T'
|
||||
} else {
|
||||
log.Println("state/hours.txt has bad timestamp:", line)
|
||||
continue
|
||||
}
|
||||
if until.Before(time.Now()) {
|
||||
nextEnabled = thisEnabled
|
||||
why = fmt.Sprint("state/hours.txt most recent timestamp:", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
|
||||
nextEnabled = false
|
||||
why = "`state/enabled` missing"
|
||||
}
|
||||
|
||||
if nextEnabled != s.Enabled {
|
||||
if (nextEnabled != s.Enabled) || (why != s.enabledWhy) {
|
||||
s.Enabled = nextEnabled
|
||||
log.Printf("Setting enabled=%v: %s", s.Enabled, why)
|
||||
s.enabledWhy = why
|
||||
log.Printf("Setting enabled=%v: %s", s.Enabled, s.enabledWhy)
|
||||
if s.Enabled {
|
||||
s.LogEvent("enabled", "", "", "", 0, why)
|
||||
s.LogEvent("enabled", "", "", 0, s.enabledWhy)
|
||||
} else {
|
||||
s.LogEvent("disabled", "", "", "", 0, why)
|
||||
s.LogEvent("disabled", "", "", 0, s.enabledWhy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TeamName returns team name given a team ID.
|
||||
func (s *State) TeamName(teamID string) (string, error) {
|
||||
teamFs := afero.NewBasePathFs(s.Fs, "teams")
|
||||
teamNameBytes, err := afero.ReadFile(teamFs, teamID)
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("Unregistered team ID: %s", teamID)
|
||||
} else if err != nil {
|
||||
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err)
|
||||
s.lock.RLock()
|
||||
name, ok := s.teamNames[teamID]
|
||||
s.lock.RUnlock()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unregistered team ID: %s", teamID)
|
||||
}
|
||||
|
||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||
return teamName, nil
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// SetTeamName writes out team name.
|
||||
// This can only be done once per team.
|
||||
func (s *State) SetTeamName(teamID, teamName string) error {
|
||||
s.lock.RLock()
|
||||
_, ok := s.teamNames[teamID]
|
||||
s.lock.RUnlock()
|
||||
if ok {
|
||||
return ErrAlreadyRegistered
|
||||
}
|
||||
|
||||
idsFile, err := s.Open("teamids.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Team IDs file does not exist" |