mirror of https://github.com/dirtbags/moth.git
Staging mostly-finished code
This commit is contained in:
parent
bb41697ba6
commit
56ea619b57
|
@ -7,6 +7,7 @@ COPY example-puzzles /target/puzzles/
|
||||||
COPY LICENSE.md /target/
|
COPY LICENSE.md /target/
|
||||||
RUN mkdir -p /target/state
|
RUN mkdir -p /target/state
|
||||||
WORKDIR /src/
|
WORKDIR /src/
|
||||||
|
RUN go get github.com/go-redis/redis/v8
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go install -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
|
# I can't use /target/bin: doing so would cause the devel server to overwrite Ubuntu's /bin
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
@ -52,6 +53,25 @@ func main() {
|
||||||
"",
|
"",
|
||||||
"Random seed to use, overrides $SEED",
|
"Random seed to use, overrides $SEED",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stateEngine := flag.String(
|
||||||
|
"state-engine",
|
||||||
|
"legacy",
|
||||||
|
"Specifiy a state engine",
|
||||||
|
)
|
||||||
|
|
||||||
|
redis_url := flag.String(
|
||||||
|
"redis-url",
|
||||||
|
"",
|
||||||
|
"URL for Redis state instance",
|
||||||
|
)
|
||||||
|
|
||||||
|
redis_db := flag.Uint64(
|
||||||
|
"redis-db",
|
||||||
|
^uint64(0),
|
||||||
|
"Database number for Redis state instance",
|
||||||
|
)
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
osfs := afero.NewOsFs()
|
osfs := afero.NewOsFs()
|
||||||
|
@ -68,7 +88,34 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var state StateProvider
|
var state StateProvider
|
||||||
state = NewState(afero.NewBasePathFs(osfs, *statePath))
|
|
||||||
|
switch engine := *stateEngine; engine {
|
||||||
|
case "redis":
|
||||||
|
redis_url_parsed := *redis_url
|
||||||
|
if redis_url_parsed == "" {
|
||||||
|
redis_url_parsed = os.Getenv("REDIS_URL")
|
||||||
|
if redis_url_parsed == "" {
|
||||||
|
log.Fatal("Redis mode was selected, but --redis-url or REDIS_URL were not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redis_db_parsed := *redis_db
|
||||||
|
if redis_db_parsed == ^uint64(0) {
|
||||||
|
redis_db_parsed_inner, err := strconv.ParseUint(os.Getenv("REDIS_DB"), 10, 64)
|
||||||
|
redis_db_parsed = redis_db_parsed_inner
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Redis mode was selected, but --redis-db or REDIS_DB were not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = NewRedisState(redis_url_parsed, int(redis_db_parsed))
|
||||||
|
default:
|
||||||
|
case "legacy":
|
||||||
|
state = NewState(afero.NewBasePathFs(osfs, *statePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if config.Devel {
|
if config.Devel {
|
||||||
state = NewDevelState(state)
|
state = NewDevelState(state)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/dirtbags/moth/pkg/award"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
REDIS_KEY_MESSAGE = "moth:message"
|
||||||
|
REDIS_KEY_TEAMS = "moth:teams"
|
||||||
|
REDIS_KEY_TEAM_IDS = "moth:team_ids"
|
||||||
|
REDIS_KEY_POINT_LOG = "moth:points"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisState struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
redis_client *redis.Client
|
||||||
|
|
||||||
|
// Enabled tracks whether the current State system is processing updates
|
||||||
|
Enabled bool
|
||||||
|
|
||||||
|
eventStream chan []string
|
||||||
|
|
||||||
|
//lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisEventEntry struct {
|
||||||
|
/*
|
||||||
|
[]string{
|
||||||
|
strconv.FormatInt(time.Now().Unix(), 10),
|
||||||
|
event,
|
||||||
|
participantID,
|
||||||
|
teamID,
|
||||||
|
cat,
|
||||||
|
strconv.Itoa(points),
|
||||||
|
},
|
||||||
|
extra...,*/
|
||||||
|
// Unix epoch time of this event
|
||||||
|
When int64
|
||||||
|
Event string
|
||||||
|
ParticipantID string
|
||||||
|
TeamID string
|
||||||
|
Category string
|
||||||
|
Points int
|
||||||
|
Extra []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON returns the award event, encoded as a list.
|
||||||
|
func (e RedisEventEntry) MarshalJSON() ([]byte, error) {
|
||||||
|
/*ao := []interface{}{
|
||||||
|
a.When,
|
||||||
|
a.TeamID,
|
||||||
|
a.Category,
|
||||||
|
a.Points,
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return json.Marshal(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes the JSON string b.
|
||||||
|
func (e RedisEventEntry) UnmarshalJSON(b []byte) error {
|
||||||
|
//r := bytes.NewReader(b)
|
||||||
|
//dec := json.NewDecoder(r)
|
||||||
|
//dec.UseNumber() // Don't use floats
|
||||||
|
json.Unmarshal(b, &e)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisState returns a new State struct backed by the given Fs
|
||||||
|
func NewRedisState(redis_addr string, redis_db int) *RedisState {
|
||||||
|
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: redis_addr,
|
||||||
|
Password: "", // no password set
|
||||||
|
DB: redis_db, // use default DB
|
||||||
|
})
|
||||||
|
|
||||||
|
s := &RedisState{
|
||||||
|
Enabled: true,
|
||||||
|
eventStream: make(chan []string, 80),
|
||||||
|
|
||||||
|
ctx: context.Background(),
|
||||||
|
redis_client: rdb,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages retrieves the current messages.
|
||||||
|
func (s *RedisState) Messages() string {
|
||||||
|
val, err := s.redis_client.Get(s.ctx, REDIS_KEY_MESSAGE).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamName returns team name given a team ID.
|
||||||
|
func (s *RedisState) TeamName(teamID string) (string, error) {
|
||||||
|
team_name, err := s.redis_client.HGet(s.ctx, REDIS_KEY_TEAMS, teamID).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unregistered team ID: %s", teamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return team_name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTeamName writes out team name.
|
||||||
|
// This can only be done once per team.
|
||||||
|
func (s *RedisState) SetTeamName(teamID, teamName string) error {
|
||||||
|
valid_id, err := s.redis_client.SIsMember(s.ctx, REDIS_KEY_TEAM_IDS, teamID).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unexpected error while validating team ID: %s", teamID)
|
||||||
|
} else if (!valid_id) {
|
||||||
|
return fmt.Errorf("team ID: (%s) not found in list of valid team IDs", teamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
success, err := s.redis_client.HSetNX(s.ctx, REDIS_KEY_TEAMS, teamID, teamName).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unexpected error while setting team ID: %s and team Name: %s", teamID, teamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Team ID: %s is already set", teamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointsLog retrieves the current points log.
|
||||||
|
func (s *RedisState) PointsLog() award.List {
|
||||||
|
redis_args := &redis.ZRangeBy{
|
||||||
|
Min: "0",
|
||||||
|
Max: "-1",
|
||||||
|
}
|
||||||
|
scores, err := s.redis_client.ZRangeByScoreWithScores(s.ctx, REDIS_KEY_POINT_LOG, redis_args).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return make(award.List, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
point_log := make(award.List, len(scores))
|
||||||
|
|
||||||
|
for _, item := range scores {
|
||||||
|
point_entry := award.T{}
|
||||||
|
|
||||||
|
point_string := strings.TrimSpace(item.Member.(string))
|
||||||
|
|
||||||
|
point_entry.When = int64(item.Score)
|
||||||
|
n, err := fmt.Sscanf(point_string, "%s %s %d", &point_entry.TeamID, &point_entry.Category, &point_entry.Points)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Do nothing
|
||||||
|
} else if n != 3 {
|
||||||
|
// Wrong number of fields, do nothing
|
||||||
|
} else {
|
||||||
|
point_log = append(point_log, point_entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return point_log
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RedisState) AwardPoints(teamID, category string, points int) error {
|
||||||
|
redis_args := redis.ZAddArgs {
|
||||||
|
LT: true,
|
||||||
|
}
|
||||||
|
awardTime := time.Now().Unix()
|
||||||
|
|
||||||
|
point_string := fmt.Sprintf("%s %s %s", teamID, category, points)
|
||||||
|
|
||||||
|
new_member := redis.Z{
|
||||||
|
Score: float64(awardTime),
|
||||||
|
Member: point_string,
|
||||||
|
}
|
||||||
|
|
||||||
|
redis_args.Members = append(redis_args.Members, new_member)
|
||||||
|
|
||||||
|
_, err := s.redis_client.ZAddArgs(s.ctx, REDIS_KEY_POINT_LOG, redis_args).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogEvent writes to the event log
|
||||||
|
func (s *RedisState) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) {
|
||||||
|
new_event := RedisEventEntry {
|
||||||
|
When: time.Now().Unix(),
|
||||||
|
Event: event,
|
||||||
|
ParticipantID: participantID,
|
||||||
|
TeamID: teamID,
|
||||||
|
Category: cat,
|
||||||
|
Points: points,
|
||||||
|
Extra: extra,
|
||||||
|
}
|
||||||
|
|
||||||
|
message := new_event.MarshalJSON()
|
||||||
|
/*
|
||||||
|
s.eventStream <- append(
|
||||||
|
[]string{
|
||||||
|
strconv.FormatInt(time.Now().Unix(), 10),
|
||||||
|
event,
|
||||||
|
participantID,
|
||||||
|
teamID,
|
||||||
|
cat,
|
||||||
|
strconv.Itoa(points),
|
||||||
|
},
|
||||||
|
extra...,
|
||||||
|
)*/
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *RedisState) Maintain(updateInterval time.Duration) {
|
||||||
|
/*
|
||||||
|
ticker := time.NewTicker(updateInterval)
|
||||||
|
s.refresh()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-s.eventStream:
|
||||||
|
s.eventWriter.Write(msg)
|
||||||
|
s.eventWriter.Flush()
|
||||||
|
s.eventWriterFile.Sync()
|
||||||
|
case <-ticker.C:
|
||||||
|
s.refresh()
|
||||||
|
case <-s.refreshNow:
|
||||||
|
s.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
Loading…
Reference in New Issue