Compare commits
6 Commits
0b22ba4bd2
...
ab3a549f78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab3a549f78 | ||
|
|
bf3b341b69 | ||
|
|
54116131b3 | ||
|
|
57dd66e510 | ||
|
|
f1282f588d | ||
|
|
706d55631b |
@@ -102,6 +102,19 @@ func (games Games) UserName(ctx context.Context, id string) (string, error) {
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (games Games) UserByName(ctx context.Context, gid, name string) (string, error) {
|
||||||
|
result := ""
|
||||||
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
||||||
|
return rows.Scan(&result)
|
||||||
|
}, `
|
||||||
|
SELECT users.uuid
|
||||||
|
FROM players
|
||||||
|
JOIN users ON players.user_uuid=users.uuid
|
||||||
|
WHERE players.game_uuid=? AND users.name=?
|
||||||
|
`, gid, name)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (a KillWord) Empty() bool {
|
func (a KillWord) Empty() bool {
|
||||||
return a == (KillWord{})
|
return a == (KillWord{})
|
||||||
}
|
}
|
||||||
@@ -156,6 +169,7 @@ func (games Games) GameByName(ctx context.Context, name string) (string, error)
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
GameState struct {
|
GameState struct {
|
||||||
|
ID string
|
||||||
Started bool
|
Started bool
|
||||||
Completed time.Time
|
Completed time.Time
|
||||||
Players map[string]PlayerState
|
Players map[string]PlayerState
|
||||||
@@ -215,6 +229,11 @@ type (
|
|||||||
KillWord KillWord
|
KillWord KillWord
|
||||||
AllKillWords AllKillWords
|
AllKillWords AllKillWords
|
||||||
}
|
}
|
||||||
|
EventGameReset struct {
|
||||||
|
Type EventType
|
||||||
|
Timestamp time.Time
|
||||||
|
ID string
|
||||||
|
}
|
||||||
AllKillWords map[string]KillWords
|
AllKillWords map[string]KillWords
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,6 +242,7 @@ const (
|
|||||||
PlayerLeave
|
PlayerLeave
|
||||||
GameComplete
|
GameComplete
|
||||||
AssignmentRotation
|
AssignmentRotation
|
||||||
|
GameReset
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event interface{ event() }
|
type Event interface{ event() }
|
||||||
@@ -231,6 +251,7 @@ func (EventPlayerJoin) event() {}
|
|||||||
func (EventPlayerLeave) event() {}
|
func (EventPlayerLeave) event() {}
|
||||||
func (EventGameComplete) event() {}
|
func (EventGameComplete) event() {}
|
||||||
func (EventAssignmentRotation) event() {}
|
func (EventAssignmentRotation) event() {}
|
||||||
|
func (EventGameReset) event() {}
|
||||||
|
|
||||||
func EventWithTime(event Event, t time.Time) Event {
|
func EventWithTime(event Event, t time.Time) Event {
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
@@ -246,6 +267,9 @@ func EventWithTime(event Event, t time.Time) Event {
|
|||||||
case EventAssignmentRotation:
|
case EventAssignmentRotation:
|
||||||
e.Timestamp = t
|
e.Timestamp = t
|
||||||
event = e
|
event = e
|
||||||
|
case EventGameReset:
|
||||||
|
e.Timestamp = t
|
||||||
|
event = e
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
@@ -296,12 +320,16 @@ func parseEvent(b []byte, timestamp time.Time) (Event, error) {
|
|||||||
var v EventAssignmentRotation
|
var v EventAssignmentRotation
|
||||||
err := json.Unmarshal(b, &v)
|
err := json.Unmarshal(b, &v)
|
||||||
return EventWithTime(v, timestamp), err
|
return EventWithTime(v, timestamp), err
|
||||||
|
case GameReset:
|
||||||
|
var v EventGameReset
|
||||||
|
err := json.Unmarshal(b, &v)
|
||||||
|
return EventWithTime(v, timestamp), err
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b)
|
return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
|
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
|
||||||
result := GameState{Players: map[string]PlayerState{}}
|
result := GameState{ID: id, Players: map[string]PlayerState{}}
|
||||||
|
|
||||||
events, err := games.GameEvents(ctx, id, time.Time{})
|
events, err := games.GameEvents(ctx, id, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -309,19 +337,16 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
switch event.(type) {
|
switch e := event.(type) {
|
||||||
case EventPlayerJoin:
|
case EventPlayerJoin:
|
||||||
playerJoin := event.(EventPlayerJoin)
|
result.Players[e.ID] = PlayerState{}
|
||||||
result.Players[playerJoin.ID] = PlayerState{}
|
|
||||||
case EventPlayerLeave:
|
case EventPlayerLeave:
|
||||||
playerLeave := event.(EventPlayerLeave)
|
delete(result.Players, e.ID)
|
||||||
delete(result.Players, playerLeave.ID)
|
|
||||||
case EventGameComplete:
|
case EventGameComplete:
|
||||||
gameComplete := event.(EventGameComplete)
|
result.Completed = e.Timestamp
|
||||||
result.Completed = gameComplete.Timestamp
|
|
||||||
case EventAssignmentRotation:
|
case EventAssignmentRotation:
|
||||||
result.Started = true
|
result.Started = true
|
||||||
assignmentRotation := event.(EventAssignmentRotation)
|
assignmentRotation := e
|
||||||
|
|
||||||
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
|
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
|
||||||
} else if _, ok := result.Players[assignmentRotation.Victim]; !ok {
|
} else if _, ok := result.Players[assignmentRotation.Victim]; !ok {
|
||||||
@@ -344,6 +369,8 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
|
|||||||
player.KillWords = v
|
player.KillWords = v
|
||||||
result.Players[k] = player
|
result.Players[k] = player
|
||||||
}
|
}
|
||||||
|
case EventGameReset:
|
||||||
|
return games.GameState(ctx, e.ID)
|
||||||
default:
|
default:
|
||||||
return GameState{}, fmt.Errorf("unknown event type %T", event)
|
return GameState{}, fmt.Errorf("unknown event type %T", event)
|
||||||
}
|
}
|
||||||
@@ -378,14 +405,8 @@ func (games Games) CreateGame(ctx context.Context, name string) (string, error)
|
|||||||
`, id, time.Now(), name)
|
`, id, time.Now(), name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player, name string) error {
|
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
|
||||||
if err := games.db.Exec(ctx, `
|
if _, err := games.UserName(ctx, player); err != nil {
|
||||||
INSERT INTO users (
|
|
||||||
uuid,
|
|
||||||
name
|
|
||||||
) VALUES (?, ?)
|
|
||||||
ON CONFLICT DO UPDATE SET name=? WHERE uuid=?;
|
|
||||||
`, player, name, name, player); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := games.db.Exec(ctx, `
|
if err := games.db.Exec(ctx, `
|
||||||
@@ -430,6 +451,32 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
|
|||||||
return games.createEvent(ctx, id, event)
|
return games.createEvent(ctx, id, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (games Games) CreateEventGameReset(ctx context.Context, gid string) error {
|
||||||
|
state, err := games.GameState(ctx, gid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name, err := games.GameName(ctx, gid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gid2, err := games.CreateGame(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for p := range state.Players {
|
||||||
|
if err := games.CreateEventPlayerJoin(ctx, gid2, p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event := EventGameReset{
|
||||||
|
Type: GameReset,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
ID: gid2,
|
||||||
|
}
|
||||||
|
return games.createEvent(ctx, gid, event)
|
||||||
|
}
|
||||||
|
|
||||||
func (words KillWords) Empty() bool {
|
func (words KillWords) Empty() bool {
|
||||||
return words.Global == (KillWord{}) && words.Assigned.IsZero() && words.Assignee == "" && words.Assignment.Empty()
|
return words.Global == (KillWord{}) && words.Assigned.IsZero() && words.Assignee == "" && words.Assignment.Empty()
|
||||||
}
|
}
|
||||||
@@ -659,3 +706,7 @@ func (games Games) createEvent(ctx context.Context, id string, v any) error {
|
|||||||
) VALUES (?, ?, ?)
|
) VALUES (?, ?, ?)
|
||||||
`, id, time.Now(), payload)
|
`, id, time.Now(), payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (games *Games) Reset(ctx context.Context, gid string) error {
|
||||||
|
return games.CreateEventGameReset(ctx, gid)
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func TestGames(t *testing.T) {
|
|||||||
t.Fatal("redundant create game didnt return same id:", id2)
|
t.Fatal("redundant create game didnt return same id:", id2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := games.CreateEventPlayerJoin(ctx, id, "p0", "player zero"); err != nil {
|
if err := games.CreateEventPlayerJoin(ctx, id, "p0"); err != nil {
|
||||||
t.Fatal("err creating event player join:", err)
|
t.Fatal("err creating event player join:", err)
|
||||||
} else if err := games.CreateEventPlayerLeave(ctx, id, "p0"); err != nil {
|
} else if err := games.CreateEventPlayerLeave(ctx, id, "p0"); err != nil {
|
||||||
t.Fatal("err creating event player leave:", err)
|
t.Fatal("err creating event player leave:", err)
|
||||||
@@ -64,12 +64,12 @@ func TestGames(t *testing.T) {
|
|||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
p := fmt.Sprintf("p%d", i+1)
|
p := fmt.Sprintf("p%d", i+1)
|
||||||
if err := games.CreateEventPlayerJoin(ctx, id, p, "player "+p); err != nil {
|
if err := games.CreateEventPlayerJoin(ctx, id, p); err != nil {
|
||||||
t.Fatal(p, "err creating event player join", err)
|
t.Fatal(p, "err creating event player join", err)
|
||||||
}
|
}
|
||||||
if name, err := games.UserName(ctx, p); err != nil {
|
if name, err := games.UserName(ctx, p); err != nil {
|
||||||
t.Fatal(p, "err getting user name", err)
|
t.Fatal(p, "err getting user name", err)
|
||||||
} else if name != "player "+p {
|
} else if name == "" {
|
||||||
t.Fatal("name wrong", name)
|
t.Fatal("name wrong", name)
|
||||||
}
|
}
|
||||||
if err := games.UpdateUserName(ctx, p, "player! "+p); err != nil {
|
if err := games.UpdateUserName(ctx, p, "player! "+p); err != nil {
|
||||||
@@ -149,6 +149,28 @@ func TestGames(t *testing.T) {
|
|||||||
} else if state.Completed.IsZero() {
|
} else if state.Completed.IsZero() {
|
||||||
t.Fatal("state.Completed is zero")
|
t.Fatal("state.Completed is zero")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := games.Reset(ctx, id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if state, err := games.GameState(ctx, id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if state.ID == id {
|
||||||
|
t.Fatal("getting state for reset game didnt return state for new game")
|
||||||
|
} else if state.Started {
|
||||||
|
t.Fatal("reset game is started", state.Started)
|
||||||
|
} else if !state.Completed.IsZero() {
|
||||||
|
t.Fatal("reset game is complete", state.Completed)
|
||||||
|
} else if len(state.Players) != 4 {
|
||||||
|
t.Fatal("reset game doesnt have all players", len(state.Players))
|
||||||
|
} else if p := state.Players["p1"]; !p.Empty() {
|
||||||
|
t.Fatal("reset game missing p1", p)
|
||||||
|
} else if p := state.Players["p2"]; !p.Empty() {
|
||||||
|
t.Fatal("reset game missing p2", p)
|
||||||
|
} else if p := state.Players["p3"]; !p.Empty() {
|
||||||
|
t.Fatal("reset game missing p3", p)
|
||||||
|
} else if p := state.Players["p4"]; !p.Empty() {
|
||||||
|
t.Fatal("reset game missing p4", p)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,5 +424,15 @@ func TestGenerateUserName(t *testing.T) {
|
|||||||
t.Fatal(name2)
|
t.Fatal(name2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := games.CreateEventPlayerJoin(context.Background(), "gid", "id"); err != nil {
|
||||||
|
t.Fatal("err creating event player join:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, err := games.UserByName(context.Background(), "gid", name); err != nil {
|
||||||
|
t.Fatal("err getting user by name:", err)
|
||||||
|
} else if id != "id" {
|
||||||
|
t.Fatal("getting user by name yielded wrong id:", id)
|
||||||
|
}
|
||||||
|
|
||||||
t.Log(name)
|
t.Log(name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +80,34 @@ func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Conte
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if killOccurred := m["k"] != ""; killOccurred {
|
} else if killOccurred := m["k"] != ""; killOccurred {
|
||||||
return fmt.Errorf("not impl: a kill occurred: %+v", m)
|
victimName := m["name"]
|
||||||
|
word := m["k"]
|
||||||
|
if word == "" {
|
||||||
|
return fmt.Errorf("expected .k")
|
||||||
|
}
|
||||||
|
|
||||||
|
killer := ugs.Session.ID
|
||||||
|
victim, err := ugs.games.UserByName(ctx, ugs.ID, victimName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var points int
|
||||||
|
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
|
||||||
|
return err
|
||||||
|
} else if global := gameState.Players[killer].KillWords.Global; global.Word == word {
|
||||||
|
points = global.Points
|
||||||
|
} else if matches := slices.DeleteFunc(gameState.Players[victim].KillWords.Publics(), func(kw KillWord) bool { return kw.Word != word }); len(matches) > 0 {
|
||||||
|
points = matches[0].Points
|
||||||
|
} else if matches := slices.DeleteFunc(gameState.Players[victim].KillWords.Privates(), func(kw KillWord) bool { return kw.Word != word }); len(matches) > 0 {
|
||||||
|
points = matches[0].Points
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("refusing unexpected .k")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ugs.games.CreateEventAssignmentRotation(ctx, ugs.ID, killer, victim, word, points); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else if isRename := m["name"] != ""; isRename {
|
} else if isRename := m["name"] != ""; isRename {
|
||||||
if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, m["name"]); err != nil {
|
if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, m["name"]); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -88,8 +116,8 @@ func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Conte
|
|||||||
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
|
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if gameState.Completed.IsZero() {
|
} else if gameState.Completed.IsZero() {
|
||||||
} else {
|
} else if err := ugs.games.Reset(ctx, ugs.ID); err != nil {
|
||||||
return fmt.Errorf("not impl: new game: %+v", m)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("UNKNOWN: %+v", m)
|
return fmt.Errorf("UNKNOWN: %+v", m)
|
||||||
@@ -105,6 +133,7 @@ func (ugs *UserGameServer) State(ctx context.Context) (UserGameState, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return UserGameState{}, err
|
return UserGameState{}, err
|
||||||
}
|
}
|
||||||
|
ugs.ID = gameState.ID
|
||||||
|
|
||||||
if complete := !gameState.Completed.IsZero(); complete {
|
if complete := !gameState.Completed.IsZero(); complete {
|
||||||
return UserGameState(gameState), nil
|
return UserGameState(gameState), nil
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestUserGameServer(t *testing.T) {
|
|||||||
pids := []string{}
|
pids := []string{}
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
pid := fmt.Sprintf("p%d", i+1)
|
pid := fmt.Sprintf("p%d", i+1)
|
||||||
if err := games.CreateEventPlayerJoin(ctx, gid, pid, "player "+pid); err != nil {
|
if err := games.CreateEventPlayerJoin(ctx, gid, pid); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pids = append(pids, pid)
|
pids = append(pids, pid)
|
||||||
|
|||||||
@@ -46,11 +46,6 @@ func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) serveV1PutParty(w http.ResponseWriter, r *http.Request) error {
|
func (s *S) serveV1PutParty(w http.ResponseWriter, r *http.Request) error {
|
||||||
userName, err := s.games.UserName(r.Context(), s.Session(r.Context()).ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
party, err := io.ReadAll(r.Body)
|
party, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -73,5 +68,5 @@ func (s *S) serveV1PutParty(w http.ResponseWriter, r *http.Request) error {
|
|||||||
if slices.Contains(games, gid) {
|
if slices.Contains(games, gid) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return s.games.CreateEventPlayerJoin(r.Context(), gid, s.Session(r.Context()).ID, userName)
|
return s.games.CreateEventPlayerJoin(r.Context(), gid, s.Session(r.Context()).ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -72,6 +72,15 @@ func (s *S) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
func (s *S) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, content-type, Content-Type")
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.Header().Set("Content-Length", "0")
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if isV1(r) || isWS(r) {
|
if isV1(r) || isWS(r) {
|
||||||
return s.serveAPI(w, r)
|
return s.serveAPI(w, r)
|
||||||
}
|
}
|
||||||
@@ -177,12 +186,12 @@ func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
|
|||||||
switch r.Method + r.URL.Path {
|
switch r.Method + r.URL.Path {
|
||||||
case "GET/v1/state/" + s.Session(ctx).ID:
|
case "GET/v1/state/" + s.Session(ctx).ID:
|
||||||
if rand.Int()%2 == 0 {
|
if rand.Int()%2 == 0 {
|
||||||
w.Write(`{"name": "foo"}`)
|
w.Write([]byte(`{"name": "foo"}`))
|
||||||
} else {
|
} else {
|
||||||
w.Write(`{"name": "bar", "party": "party name"}`)
|
w.Write([]byte(`{"name": "bar", "party": "party name"}`))
|
||||||
}
|
}
|
||||||
case "PUT/v1/state/" + s.Session(ctx).ID + "/party":
|
case "PUT/v1/state/" + s.Session(ctx).ID + "/party":
|
||||||
w.Write(`{}`)
|
w.Write([]byte(`{}`))
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user