Update project documentation and improve frontend functionality

- Updated the backend documentation in CONTRIBUTING.md and README.md to reflect changes in application structure and API endpoints.
- Enhanced the frontend components in MapView.vue for better handling of context menu actions.
- Added new types and interfaces in TypeScript for improved type safety in the frontend.
- Introduced new utility classes for managing characters and markers in the map.
- Updated .gitignore to include .vscode directory for better development environment management.
This commit is contained in:
2026-02-24 23:32:50 +03:00
parent 605a31567e
commit 82cb8a13f5
39 changed files with 1788 additions and 2631 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
package app
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"go.etcd.io/bbolt"
)
type mapData struct {
Grids map[string]string
Markers map[string][]Marker
}
func (a *App) export(rw http.ResponseWriter, req *http.Request) {
s := a.getSession(req)
if s == nil || !s.Auths.Has(AUTH_ADMIN) {
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
return
}
rw.Header().Set("Content-Type", "application/zip")
rw.Header().Set("Content-Disposition", "attachment; filename=\"griddata.zip\"")
zw := zip.NewWriter(rw)
defer zw.Close()
err := a.db.Update(func(tx *bbolt.Tx) error {
maps := map[int]mapData{}
gridMap := map[string]int{}
grids := tx.Bucket([]byte("grids"))
if grids == nil {
return nil
}
tiles := tx.Bucket([]byte("tiles"))
if tiles == nil {
return nil
}
err := grids.ForEach(func(k, v []byte) error {
gd := GridData{}
err := json.Unmarshal(v, &gd)
if err != nil {
return err
}
md, ok := maps[gd.Map]
if !ok {
md = mapData{
Grids: map[string]string{},
Markers: map[string][]Marker{},
}
maps[gd.Map] = md
}
md.Grids[gd.Coord.Name()] = gd.ID
gridMap[gd.ID] = gd.Map
mapb := tiles.Bucket([]byte(strconv.Itoa(gd.Map)))
if mapb == nil {
return nil
}
zoom := mapb.Bucket([]byte("0"))
if zoom == nil {
return nil
}
tdraw := zoom.Get([]byte(gd.Coord.Name()))
if tdraw == nil {
return nil
}
td := TileData{}
err = json.Unmarshal(tdraw, &td)
if err != nil {
return err
}
w, err := zw.Create(fmt.Sprintf("%d/%s.png", gd.Map, gd.ID))
if err != nil {
return err
}
f, err := os.Open(filepath.Join(a.gridStorage, td.File))
if err != nil {
return err
}
_, err = io.Copy(w, f)
f.Close()
return err
})
if err != nil {
return err
}
err = func() error {
markersb := tx.Bucket([]byte("markers"))
if markersb == nil {
return nil
}
markersgrid := markersb.Bucket([]byte("grid"))
if markersgrid == nil {
return nil
}
return markersgrid.ForEach(func(k, v []byte) error {
marker := Marker{}
err := json.Unmarshal(v, &marker)
if err != nil {
return nil
}
if _, ok := maps[gridMap[marker.GridID]]; ok {
maps[gridMap[marker.GridID]].Markers[marker.GridID] = append(maps[gridMap[marker.GridID]].Markers[marker.GridID], marker)
}
return nil
})
}()
if err != nil {
return err
}
for mapid, mapdata := range maps {
w, err := zw.Create(fmt.Sprintf("%d/grids.json", mapid))
if err != nil {
return err
}
json.NewEncoder(w).Encode(mapdata)
}
return nil
})
if err != nil {
log.Println(err)
}
}

View File

@@ -0,0 +1,48 @@
package app
import (
"encoding/json"
"fmt"
"log"
"net/http"
"go.etcd.io/bbolt"
)
func (a *App) hideMarker(rw http.ResponseWriter, req *http.Request) {
if a.requireAdmin(rw, req) == nil {
return
}
err := a.db.Update(func(tx *bbolt.Tx) error {
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
if err != nil {
return err
}
grid, err := mb.CreateBucketIfNotExists([]byte("grid"))
if err != nil {
return err
}
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
if err != nil {
return err
}
key := idB.Get([]byte(req.FormValue("id")))
if key == nil {
return fmt.Errorf("Could not find key %s", req.FormValue("id"))
}
raw := grid.Get(key)
if raw == nil {
return fmt.Errorf("Could not find key %s", string(key))
}
m := Marker{}
json.Unmarshal(raw, &m)
m.Hidden = true
raw, _ = json.Marshal(m)
grid.Put(key, raw)
return nil
})
if err != nil {
log.Println(err)
}
}

305
internal/app/admin_merge.go Normal file
View File

@@ -0,0 +1,305 @@
package app
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"go.etcd.io/bbolt"
)
func (a *App) merge(rw http.ResponseWriter, req *http.Request) {
if s := a.getSession(req); s == nil || !s.Auths.Has(AUTH_ADMIN) {
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
return
}
err := req.ParseMultipartForm(1024 * 1024 * 500)
if err != nil {
log.Println(err)
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
mergef, hdr, err := req.FormFile("merge")
if err != nil {
log.Println(err)
http.Error(rw, "request error", http.StatusBadRequest)
return
}
zr, err := zip.NewReader(mergef, hdr.Size)
if err != nil {
log.Println(err)
http.Error(rw, "request error", http.StatusBadRequest)
return
}
ops := []struct {
mapid int
x, y int
f string
}{}
newTiles := map[string]struct{}{}
err = a.db.Update(func(tx *bbolt.Tx) error {
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
if err != nil {
return err
}
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
if err != nil {
return err
}
mgrid, err := mb.CreateBucketIfNotExists([]byte("grid"))
if err != nil {
return err
}
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
if err != nil {
return err
}
configb, err := tx.CreateBucketIfNotExists([]byte("config"))
if err != nil {
return err
}
for _, fhdr := range zr.File {
if strings.HasSuffix(fhdr.Name, ".json") {
f, err := fhdr.Open()
if err != nil {
return err
}
md := mapData{}
err = json.NewDecoder(f).Decode(&md)
if err != nil {
return err
}
for _, ms := range md.Markers {
for _, mraw := range ms {
key := []byte(fmt.Sprintf("%s_%d_%d", mraw.GridID, mraw.Position.X, mraw.Position.Y))
if mgrid.Get(key) != nil {
continue
}
if mraw.Image == "" {
mraw.Image = "gfx/terobjs/mm/custom"
}
id, err := idB.NextSequence()
if err != nil {
return err
}
idKey := []byte(strconv.Itoa(int(id)))
m := Marker{
Name: mraw.Name,
ID: int(id),
GridID: mraw.GridID,
Position: Position{
X: mraw.Position.X,
Y: mraw.Position.Y,
},
Image: mraw.Image,
}
raw, _ := json.Marshal(m)
mgrid.Put(key, raw)
idB.Put(idKey, key)
}
}
mapB, err := tx.CreateBucketIfNotExists([]byte("maps"))
if err != nil {
return err
}
newGrids := map[Coord]string{}
maps := map[int]struct{ X, Y int }{}
for k, v := range md.Grids {
c := Coord{}
_, err := fmt.Sscanf(k, "%d_%d", &c.X, &c.Y)
if err != nil {
return err
}
newGrids[c] = v
gridRaw := grids.Get([]byte(v))
if gridRaw != nil {
gd := GridData{}
json.Unmarshal(gridRaw, &gd)
maps[gd.Map] = struct{ X, Y int }{gd.Coord.X - c.X, gd.Coord.Y - c.Y}
}
}
if len(maps) == 0 {
seq, err := mapB.NextSequence()
if err != nil {
return err
}
mi := MapInfo{
ID: int(seq),
Name: strconv.Itoa(int(seq)),
Hidden: configb.Get([]byte("defaultHide")) != nil,
}
raw, _ := json.Marshal(mi)
err = mapB.Put([]byte(strconv.Itoa(int(seq))), raw)
if err != nil {
return err
}
for c, grid := range newGrids {
cur := GridData{}
cur.ID = grid
cur.Map = int(seq)
cur.Coord = c
raw, err := json.Marshal(cur)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
}
continue
}
mapid := -1
offset := struct{ X, Y int }{}
for id, off := range maps {
mi := MapInfo{}
mraw := mapB.Get([]byte(strconv.Itoa(id)))
if mraw != nil {
json.Unmarshal(mraw, &mi)
}
if mi.Priority {
mapid = id
offset = off
break
}
if id < mapid || mapid == -1 {
mapid = id
offset = off
}
}
for c, grid := range newGrids {
cur := GridData{}
if curRaw := grids.Get([]byte(grid)); curRaw != nil {
continue
}
cur.ID = grid
cur.Map = mapid
cur.Coord.X = c.X + offset.X
cur.Coord.Y = c.Y + offset.Y
raw, err := json.Marshal(cur)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
}
if len(maps) > 1 {
grids.ForEach(func(k, v []byte) error {
gd := GridData{}
json.Unmarshal(v, &gd)
if gd.Map == mapid {
return nil
}
if merge, ok := maps[gd.Map]; ok {
var td *TileData
mapb, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(gd.Map)))
if err != nil {
return err
}
zoom, err := mapb.CreateBucketIfNotExists([]byte(strconv.Itoa(0)))
if err != nil {
return err
}
tileraw := zoom.Get([]byte(gd.Coord.Name()))
if tileraw != nil {
json.Unmarshal(tileraw, &td)
}
gd.Map = mapid
gd.Coord.X += offset.X - merge.X
gd.Coord.Y += offset.Y - merge.Y
raw, _ := json.Marshal(gd)
if td != nil {
ops = append(ops, struct {
mapid int
x int
y int
f string
}{
mapid: mapid,
x: gd.Coord.X,
y: gd.Coord.Y,
f: td.File,
})
}
grids.Put(k, raw)
}
return nil
})
}
for mergeid, merge := range maps {
if mapid == mergeid {
continue
}
mapB.Delete([]byte(strconv.Itoa(mergeid)))
log.Println("Reporting merge", mergeid, mapid)
a.reportMerge(mergeid, mapid, Coord{X: offset.X - merge.X, Y: offset.Y - merge.Y})
}
} else if strings.HasSuffix(fhdr.Name, ".png") {
os.MkdirAll(filepath.Join(a.gridStorage, "grids"), 0777)
f, err := os.Create(filepath.Join(a.gridStorage, "grids", filepath.Base(fhdr.Name)))
if err != nil {
return err
}
r, err := fhdr.Open()
if err != nil {
f.Close()
return err
}
io.Copy(f, r)
r.Close()
f.Close()
newTiles[strings.TrimSuffix(filepath.Base(fhdr.Name), ".png")] = struct{}{}
}
}
for gid := range newTiles {
gridRaw := grids.Get([]byte(gid))
if gridRaw != nil {
gd := GridData{}
json.Unmarshal(gridRaw, &gd)
ops = append(ops, struct {
mapid int
x int
y int
f string
}{
mapid: gd.Map,
x: gd.Coord.X,
y: gd.Coord.Y,
f: filepath.Join("grids", gid+".png"),
})
}
}
return nil
})
if err != nil {
log.Println(err)
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
for _, op := range ops {
a.SaveTile(op.mapid, Coord{X: op.x, Y: op.y}, 0, op.f, time.Now().UnixNano())
}
a.doRebuildZooms()
}

View File

@@ -0,0 +1,52 @@
package app
import (
"encoding/json"
"fmt"
"os"
"time"
"go.etcd.io/bbolt"
)
type zoomproc struct {
c Coord
m int
}
func (a *App) doRebuildZooms() {
needProcess := map[zoomproc]struct{}{}
saveGrid := map[zoomproc]string{}
a.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte("grids"))
if b == nil {
return nil
}
b.ForEach(func(k, v []byte) error {
grid := GridData{}
json.Unmarshal(v, &grid)
needProcess[zoomproc{grid.Coord.Parent(), grid.Map}] = struct{}{}
saveGrid[zoomproc{grid.Coord, grid.Map}] = grid.ID
return nil
})
tx.DeleteBucket([]byte("tiles"))
return nil
})
for g, id := range saveGrid {
f := fmt.Sprintf("%s/grids/%s.png", a.gridStorage, id)
if _, err := os.Stat(f); err != nil {
continue
}
a.SaveTile(g.m, g.c, 0, fmt.Sprintf("grids/%s.png", id), time.Now().UnixNano())
}
for z := 1; z <= 5; z++ {
process := needProcess
needProcess = map[zoomproc]struct{}{}
for p := range process {
a.updateZoomLevel(p.m, p.c, z)
needProcess[zoomproc{p.c.Parent(), p.m}] = struct{}{}
}
}
}

188
internal/app/admin_tiles.go Normal file
View File

@@ -0,0 +1,188 @@
package app
import (
"encoding/json"
"net/http"
"strconv"
"time"
"go.etcd.io/bbolt"
)
func (a *App) wipeTile(rw http.ResponseWriter, req *http.Request) {
if a.requireAdmin(rw, req) == nil {
return
}
mraw := req.FormValue("map")
mapid, err := strconv.Atoi(mraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
xraw := req.FormValue("x")
x, err := strconv.Atoi(xraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
yraw := req.FormValue("y")
y, err := strconv.Atoi(yraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
c := Coord{
X: x,
Y: y,
}
a.db.Update(func(tx *bbolt.Tx) error {
grids := tx.Bucket([]byte("grids"))
if grids == nil {
return nil
}
ids := [][]byte{}
err := grids.ForEach(func(k, v []byte) error {
g := GridData{}
err := json.Unmarshal(v, &g)
if err != nil {
return err
}
if g.Coord == c && g.Map == mapid {
ids = append(ids, k)
}
return nil
})
if err != nil {
return err
}
for _, id := range ids {
grids.Delete(id)
}
return nil
})
a.SaveTile(mapid, c, 0, "", -1)
for z := 1; z <= 5; z++ {
c = c.Parent()
a.updateZoomLevel(mapid, c, z)
}
rw.WriteHeader(200)
}
func (a *App) setCoords(rw http.ResponseWriter, req *http.Request) {
if a.requireAdmin(rw, req) == nil {
return
}
mraw := req.FormValue("map")
mapid, err := strconv.Atoi(mraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
fxraw := req.FormValue("fx")
fx, err := strconv.Atoi(fxraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
fyraw := req.FormValue("fy")
fy, err := strconv.Atoi(fyraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
fc := Coord{
X: fx,
Y: fy,
}
txraw := req.FormValue("tx")
tx, err := strconv.Atoi(txraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
tyraw := req.FormValue("ty")
ty, err := strconv.Atoi(tyraw)
if err != nil {
http.Error(rw, "coord parse failed", http.StatusBadRequest)
return
}
tc := Coord{
X: tx,
Y: ty,
}
diff := Coord{
X: tc.X - fc.X,
Y: tc.Y - fc.Y,
}
tds := []*TileData{}
a.db.Update(func(tx *bbolt.Tx) error {
grids := tx.Bucket([]byte("grids"))
if grids == nil {
return nil
}
tiles := tx.Bucket([]byte("tiles"))
if tiles == nil {
return nil
}
mapZooms := tiles.Bucket([]byte(strconv.Itoa(mapid)))
if mapZooms == nil {
return nil
}
mapTiles := mapZooms.Bucket([]byte("0"))
err := grids.ForEach(func(k, v []byte) error {
g := GridData{}
err := json.Unmarshal(v, &g)
if err != nil {
return err
}
if g.Map == mapid {
g.Coord.X += diff.X
g.Coord.Y += diff.Y
raw, _ := json.Marshal(g)
grids.Put(k, raw)
}
return nil
})
if err != nil {
return err
}
err = mapTiles.ForEach(func(k, v []byte) error {
td := &TileData{}
err := json.Unmarshal(v, &td)
if err != nil {
return err
}
td.Coord.X += diff.X
td.Coord.Y += diff.Y
tds = append(tds, td)
return nil
})
if err != nil {
return err
}
err = tiles.DeleteBucket([]byte(strconv.Itoa(mapid)))
if err != nil {
return err
}
return nil
})
needProcess := map[zoomproc]struct{}{}
for _, td := range tds {
a.SaveTile(td.MapID, td.Coord, td.Zoom, td.File, time.Now().UnixNano())
needProcess[zoomproc{c: Coord{X: td.Coord.X, Y: td.Coord.Y}.Parent(), m: td.MapID}] = struct{}{}
}
for z := 1; z <= 5; z++ {
process := needProcess
needProcess = map[zoomproc]struct{}{}
for p := range process {
a.updateZoomLevel(p.m, p.c, z)
needProcess[zoomproc{p.c.Parent(), p.m}] = struct{}{}
}
}
rw.WriteHeader(200)
}

View File

@@ -93,24 +93,6 @@ func (a *App) apiLogin(rw http.ResponseWriter, req *http.Request) {
})
}
// setupRequired returns true if no users exist (first run).
func (a *App) setupRequired() bool {
var required bool
a.db.View(func(tx *bbolt.Tx) error {
ub := tx.Bucket([]byte("users"))
if ub == nil {
required = true
return nil
}
if ub.Stats().KeyN == 0 {
required = true
return nil
}
return nil
})
return required
}
func (a *App) apiSetup(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
@@ -165,22 +147,6 @@ func (a *App) apiMe(rw http.ResponseWriter, req *http.Request) {
json.NewEncoder(rw).Encode(out)
}
// createSession creates a session for username, returns session ID or empty string.
func (a *App) createSession(username string, tempAdmin bool) string {
session := make([]byte, 32)
if _, err := rand.Read(session); err != nil {
return ""
}
sid := hex.EncodeToString(session)
s := &Session{
ID: sid,
Username: username,
TempAdmin: tempAdmin,
}
a.saveSession(s)
return sid
}
// --- Cabinet API ---
func (a *App) apiMeTokens(rw http.ResponseWriter, req *http.Request) {
@@ -283,15 +249,6 @@ func (a *App) setUserPassword(username, pass string) error {
// --- Admin API (require admin auth) ---
func (a *App) requireAdmin(rw http.ResponseWriter, req *http.Request) *Session {
s := a.getSession(req)
if s == nil || !s.Auths.Has(AUTH_ADMIN) {
rw.WriteHeader(http.StatusUnauthorized)
return nil
}
return s
}
func (a *App) apiAdminUsers(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
@@ -691,28 +648,6 @@ func (a *App) apiAdminMerge(rw http.ResponseWriter, req *http.Request) {
a.merge(rw, req)
}
// --- Redirects (for old URLs) ---
func (a *App) redirectRoot(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
http.NotFound(rw, req)
return
}
if a.setupRequired() {
http.Redirect(rw, req, "/map/setup", http.StatusFound)
return
}
http.Redirect(rw, req, "/map/profile", http.StatusFound)
}
func (a *App) redirectLogin(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/login" {
http.NotFound(rw, req)
return
}
http.Redirect(rw, req, "/map/login", http.StatusFound)
}
// --- API router: /map/api/... ---
func (a *App) apiRouter(rw http.ResponseWriter, req *http.Request) {
@@ -822,11 +757,5 @@ func (a *App) apiRouter(rw http.ResponseWriter, req *http.Request) {
return
}
// POST admin/users (create)
if path == "admin/users" && req.Method == http.MethodPost {
a.apiAdminUserPost(rw, req)
return
}
http.Error(rw, "not found", http.StatusNotFound)
}

View File

@@ -1,17 +1,13 @@
package app
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"sync"
"time"
"github.com/andyleap/hnh-map/webapp"
"go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
// App is the main application (map server) state.
@@ -23,8 +19,6 @@ type App struct {
characters map[string]Character
chmu sync.RWMutex
*webapp.WebApp
gridUpdates topic
mergeUpdates mergeTopic
}
@@ -32,16 +26,11 @@ type App struct {
// NewApp creates an App with the given storage paths and database.
// frontendRoot is the directory for the map SPA (e.g. "frontend").
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
w, err := webapp.New()
if err != nil {
return nil, err
}
return &App{
gridStorage: gridStorage,
frontendRoot: frontendRoot,
db: db,
characters: make(map[string]Character),
WebApp: w,
frontendRoot: frontendRoot,
db: db,
characters: make(map[string]Character),
}, nil
}
@@ -144,110 +133,10 @@ type User struct {
Tokens []string
}
func (a *App) getSession(req *http.Request) *Session {
c, err := req.Cookie("session")
if err != nil {
return nil
}
var s *Session
a.db.View(func(tx *bbolt.Tx) error {
sessions := tx.Bucket([]byte("sessions"))
if sessions == nil {
return nil
}
session := sessions.Get([]byte(c.Value))
if session == nil {
return nil
}
err := json.Unmarshal(session, &s)
if err != nil {
return err
}
if s.TempAdmin {
s.Auths = Auths{AUTH_ADMIN}
return nil
}
users := tx.Bucket([]byte("users"))
if users == nil {
return nil
}
raw := users.Get([]byte(s.Username))
if raw == nil {
s = nil
return nil
}
u := User{}
err = json.Unmarshal(raw, &u)
if err != nil {
s = nil
return err
}
s.Auths = u.Auths
return nil
})
return s
}
func (a *App) deleteSession(s *Session) {
a.db.Update(func(tx *bbolt.Tx) error {
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
if err != nil {
return err
}
return sessions.Delete([]byte(s.ID))
})
}
func (a *App) saveSession(s *Session) {
a.db.Update(func(tx *bbolt.Tx) error {
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
if err != nil {
return err
}
buf, err := json.Marshal(s)
if err != nil {
return err
}
return sessions.Put([]byte(s.ID), buf)
})
}
type Page struct {
Title string `json:"title"`
}
func (a *App) getPage(req *http.Request) Page {
p := Page{}
a.db.View(func(tx *bbolt.Tx) error {
c := tx.Bucket([]byte("config"))
if c == nil {
return nil
}
p.Title = string(c.Get([]byte("title")))
return nil
})
return p
}
func (a *App) getUser(user, pass string) (u *User) {
a.db.View(func(tx *bbolt.Tx) error {
users := tx.Bucket([]byte("users"))
if users == nil {
return nil
}
raw := users.Get([]byte(user))
if raw != nil {
json.Unmarshal(raw, &u)
if bcrypt.CompareHashAndPassword(u.Pass, []byte(pass)) != nil {
u = nil
return nil
}
}
return nil
})
return u
}
// serveMapFrontend serves the map SPA: static files from frontend, fallback to index.html for client-side routes.
func (a *App) serveMapFrontend(rw http.ResponseWriter, req *http.Request) {
path := req.URL.Path
@@ -306,23 +195,10 @@ func (a *App) RegisterRoutes() {
http.HandleFunc("/client/", a.client)
http.HandleFunc("/login", a.redirectLogin)
http.HandleFunc("/logout", a.logout)
http.HandleFunc("/logout", a.redirectLogout)
http.HandleFunc("/admin", a.redirectAdmin)
http.HandleFunc("/admin/", a.redirectAdmin)
http.HandleFunc("/", a.redirectRoot)
http.HandleFunc("/generateToken", a.generateToken)
http.HandleFunc("/password", a.changePassword)
http.HandleFunc("/admin/", a.admin)
http.HandleFunc("/admin/user", a.adminUser)
http.HandleFunc("/admin/deleteUser", a.deleteUser)
http.HandleFunc("/admin/wipe", a.wipe)
http.HandleFunc("/admin/setPrefix", a.setPrefix)
http.HandleFunc("/admin/setDefaultHide", a.setDefaultHide)
http.HandleFunc("/admin/setTitle", a.setTitle)
http.HandleFunc("/admin/rebuildZooms", a.rebuildZooms)
http.HandleFunc("/admin/export", a.export)
http.HandleFunc("/admin/merge", a.merge)
http.HandleFunc("/admin/map", a.adminMap)
http.HandleFunc("/admin/mapic", a.adminICMap)
http.HandleFunc("/map/api/", a.apiRouter)
http.HandleFunc("/map/updates", a.watchGridUpdates)

154
internal/app/auth.go Normal file
View File

@@ -0,0 +1,154 @@
package app
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
func (a *App) getSession(req *http.Request) *Session {
c, err := req.Cookie("session")
if err != nil {
return nil
}
var s *Session
a.db.View(func(tx *bbolt.Tx) error {
sessions := tx.Bucket([]byte("sessions"))
if sessions == nil {
return nil
}
session := sessions.Get([]byte(c.Value))
if session == nil {
return nil
}
err := json.Unmarshal(session, &s)
if err != nil {
return err
}
if s.TempAdmin {
s.Auths = Auths{AUTH_ADMIN}
return nil
}
users := tx.Bucket([]byte("users"))
if users == nil {
return nil
}
raw := users.Get([]byte(s.Username))
if raw == nil {
s = nil
return nil
}
u := User{}
err = json.Unmarshal(raw, &u)
if err != nil {
s = nil
return err
}
s.Auths = u.Auths
return nil
})
return s
}
func (a *App) deleteSession(s *Session) {
a.db.Update(func(tx *bbolt.Tx) error {
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
if err != nil {
return err
}
return sessions.Delete([]byte(s.ID))
})
}
func (a *App) saveSession(s *Session) {
a.db.Update(func(tx *bbolt.Tx) error {
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
if err != nil {
return err
}
buf, err := json.Marshal(s)
if err != nil {
return err
}
return sessions.Put([]byte(s.ID), buf)
})
}
func (a *App) getPage(req *http.Request) Page {
p := Page{}
a.db.View(func(tx *bbolt.Tx) error {
c := tx.Bucket([]byte("config"))
if c == nil {
return nil
}
p.Title = string(c.Get([]byte("title")))
return nil
})
return p
}
func (a *App) getUser(user, pass string) (u *User) {
a.db.View(func(tx *bbolt.Tx) error {
users := tx.Bucket([]byte("users"))
if users == nil {
return nil
}
raw := users.Get([]byte(user))
if raw != nil {
json.Unmarshal(raw, &u)
if bcrypt.CompareHashAndPassword(u.Pass, []byte(pass)) != nil {
u = nil
return nil
}
}
return nil
})
return u
}
// createSession creates a session for username, returns session ID or empty string.
func (a *App) createSession(username string, tempAdmin bool) string {
session := make([]byte, 32)
if _, err := rand.Read(session); err != nil {
return ""
}
sid := hex.EncodeToString(session)
s := &Session{
ID: sid,
Username: username,
TempAdmin: tempAdmin,
}
a.saveSession(s)
return sid
}
// setupRequired returns true if no users exist (first run).
func (a *App) setupRequired() bool {
var required bool
a.db.View(func(tx *bbolt.Tx) error {
ub := tx.Bucket([]byte("users"))
if ub == nil {
required = true
return nil
}
if ub.Stats().KeyN == 0 {
required = true
return nil
}
return nil
})
return required
}
func (a *App) requireAdmin(rw http.ResponseWriter, req *http.Request) *Session {
s := a.getSession(req)
if s == nil || !s.Auths.Has(AUTH_ADMIN) {
rw.WriteHeader(http.StatusUnauthorized)
return nil
}
return s
}

View File

@@ -4,20 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"image"
"image/png"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"golang.org/x/image/draw"
"go.etcd.io/bbolt"
)
@@ -80,8 +68,6 @@ func (a *App) client(rw http.ResponseWriter, req *http.Request) {
a.updatePositions(rw, req)
case "markerUpdate":
a.uploadMarkers(rw, req)
/*case "mapData":
a.mapdataIndex(rw, req)*/
case "":
http.Redirect(rw, req, "/map/", 302)
case "checkVersion":
@@ -95,151 +81,6 @@ func (a *App) client(rw http.ResponseWriter, req *http.Request) {
}
}
func (a *App) updatePositions(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
craws := map[string]struct {
Name string
GridID string
Coords struct {
X, Y int
}
Type string
}{}
buf, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Println("Error reading position update json: ", err)
return
}
err = json.Unmarshal(buf, &craws)
if err != nil {
log.Println("Error decoding position update json: ", err)
log.Println("Original json: ", string(buf))
return
}
a.db.View(func(tx *bbolt.Tx) error {
grids := tx.Bucket([]byte("grids"))
if grids == nil {
return nil
}
a.chmu.Lock()
defer a.chmu.Unlock()
for id, craw := range craws {
grid := grids.Get([]byte(craw.GridID))
if grid == nil {
return nil
}
gd := GridData{}
json.Unmarshal(grid, &gd)
idnum, _ := strconv.Atoi(id)
c := Character{
Name: craw.Name,
ID: idnum,
Map: gd.Map,
Position: Position{
X: craw.Coords.X + (gd.Coord.X * 100),
Y: craw.Coords.Y + (gd.Coord.Y * 100),
},
Type: craw.Type,
updated: time.Now(),
}
old, ok := a.characters[id]
if !ok {
a.characters[id] = c
} else {
if old.Type == "player" {
if c.Type == "player" {
a.characters[id] = c
} else {
old.Position = c.Position
a.characters[id] = old
}
} else if old.Type != "unknown" {
if c.Type != "unknown" {
a.characters[id] = c
} else {
old.Position = c.Position
a.characters[id] = old
}
} else {
a.characters[id] = c
}
}
}
return nil
})
}
func (a *App) uploadMarkers(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
markers := []struct {
Name string
GridID string
X, Y int
Image string
Type string
Color string
}{}
buf, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Println("Error reading marker json: ", err)
return
}
err = json.Unmarshal(buf, &markers)
if err != nil {
log.Println("Error decoding marker json: ", err)
log.Println("Original json: ", string(buf))
return
}
err = a.db.Update(func(tx *bbolt.Tx) error {
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
if err != nil {
return err
}
grid, err := mb.CreateBucketIfNotExists([]byte("grid"))
if err != nil {
return err
}
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
if err != nil {
return err
}
for _, mraw := range markers {
key := []byte(fmt.Sprintf("%s_%d_%d", mraw.GridID, mraw.X, mraw.Y))
if grid.Get(key) != nil {
continue
}
if mraw.Image == "" {
mraw.Image = "gfx/terobjs/mm/custom"
}
id, err := idB.NextSequence()
if err != nil {
return err
}
idKey := []byte(strconv.Itoa(int(id)))
m := Marker{
Name: mraw.Name,
ID: int(id),
GridID: mraw.GridID,
Position: Position{
X: mraw.X,
Y: mraw.Y,
},
Image: mraw.Image,
}
raw, _ := json.Marshal(m)
grid.Put(key, raw)
idB.Put(idKey, key)
}
return nil
})
if err != nil {
log.Println("Error update db: ", err)
return
}
}
func (a *App) locate(rw http.ResponseWriter, req *http.Request) {
grid := req.FormValue("gridID")
err := a.db.View(func(tx *bbolt.Tx) error {
@@ -263,444 +104,3 @@ func (a *App) locate(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(404)
}
}
type GridUpdate struct {
Grids [][]string `json:"grids"`
}
type GridRequest struct {
GridRequests []string `json:"gridRequests"`
Map int `json:"map"`
Coords Coord `json:"coords"`
}
func (a *App) gridUpdate(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
dec := json.NewDecoder(req.Body)
grup := GridUpdate{}
err := dec.Decode(&grup)
if err != nil {
log.Println("Error decoding grid request json: ", err)
http.Error(rw, "Error decoding request", http.StatusBadRequest)
return
}
log.Println(grup)
ops := []struct {
mapid int
x, y int
f string
}{}
greq := GridRequest{}
err = a.db.Update(func(tx *bbolt.Tx) error {
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
if err != nil {
return err
}
mapB, err := tx.CreateBucketIfNotExists([]byte("maps"))
if err != nil {
return err
}
configb, err := tx.CreateBucketIfNotExists([]byte("config"))
if err != nil {
return err
}
maps := map[int]struct{ X, Y int }{}
for x, row := range grup.Grids {
for y, grid := range row {
gridRaw := grids.Get([]byte(grid))
if gridRaw != nil {
gd := GridData{}
json.Unmarshal(gridRaw, &gd)
maps[gd.Map] = struct{ X, Y int }{gd.Coord.X - x, gd.Coord.Y - y}
}
}
}
if len(maps) == 0 {
seq, err := mapB.NextSequence()
if err != nil {
return err
}
mi := MapInfo{
ID: int(seq),
Name: strconv.Itoa(int(seq)),
Hidden: configb.Get([]byte("defaultHide")) != nil,
}
raw, _ := json.Marshal(mi)
err = mapB.Put([]byte(strconv.Itoa(int(seq))), raw)
if err != nil {
return err
}
log.Println("Client made mapid ", seq)
for x, row := range grup.Grids {
for y, grid := range row {
cur := GridData{}
cur.ID = grid
cur.Map = int(seq)
cur.Coord.X = x - 1
cur.Coord.Y = y - 1
raw, err := json.Marshal(cur)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
greq.GridRequests = append(greq.GridRequests, grid)
}
}
greq.Coords = Coord{0, 0}
return nil
}
mapid := -1
offset := struct{ X, Y int }{}
for id, off := range maps {
mi := MapInfo{}
mraw := mapB.Get([]byte(strconv.Itoa(id)))
if mraw != nil {
json.Unmarshal(mraw, &mi)
}
if mi.Priority {
mapid = id
offset = off
break
}
if id < mapid || mapid == -1 {
mapid = id
offset = off
}
}
log.Println("Client in mapid ", mapid)
for x, row := range grup.Grids {
for y, grid := range row {
cur := GridData{}
if curRaw := grids.Get([]byte(grid)); curRaw != nil {
json.Unmarshal(curRaw, &cur)
if time.Now().After(cur.NextUpdate) {
greq.GridRequests = append(greq.GridRequests, grid)
}
continue
}
cur.ID = grid
cur.Map = mapid
cur.Coord.X = x + offset.X
cur.Coord.Y = y + offset.Y
raw, err := json.Marshal(cur)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
greq.GridRequests = append(greq.GridRequests, grid)
}
}
if len(grup.Grids) >= 2 && len(grup.Grids[1]) >= 2 {
if curRaw := grids.Get([]byte(grup.Grids[1][1])); curRaw != nil {
cur := GridData{}
json.Unmarshal(curRaw, &cur)
greq.Map = cur.Map
greq.Coords = cur.Coord
}
}
if len(maps) > 1 {
grids.ForEach(func(k, v []byte) error {
gd := GridData{}
json.Unmarshal(v, &gd)
if gd.Map == mapid {
return nil
}
if merge, ok := maps[gd.Map]; ok {
var td *TileData
mapb, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(gd.Map)))
if err != nil {
return err
}
zoom, err := mapb.CreateBucketIfNotExists([]byte(strconv.Itoa(0)))
if err != nil {
return err
}
tileraw := zoom.Get([]byte(gd.Coord.Name()))
if tileraw != nil {
json.Unmarshal(tileraw, &td)
}
gd.Map = mapid
gd.Coord.X += offset.X - merge.X
gd.Coord.Y += offset.Y - merge.Y
raw, _ := json.Marshal(gd)
if td != nil {
ops = append(ops, struct {
mapid int
x int
y int
f string
}{
mapid: mapid,
x: gd.Coord.X,
y: gd.Coord.Y,
f: td.File,
})
}
grids.Put(k, raw)
}
return nil
})
}
for mergeid, merge := range maps {
if mapid == mergeid {
continue
}
mapB.Delete([]byte(strconv.Itoa(mergeid)))
log.Println("Reporting merge", mergeid, mapid)
a.reportMerge(mergeid, mapid, Coord{X: offset.X - merge.X, Y: offset.Y - merge.Y})
}
return nil
})
if err != nil {
log.Println(err)
return
}
needProcess := map[zoomproc]struct{}{}
for _, op := range ops {
a.SaveTile(op.mapid, Coord{X: op.x, Y: op.y}, 0, op.f, time.Now().UnixNano())
needProcess[zoomproc{c: Coord{X: op.x, Y: op.y}.Parent(), m: op.mapid}] = struct{}{}
}
for z := 1; z <= 5; z++ {
process := needProcess
needProcess = map[zoomproc]struct{}{}
for p := range process {
a.updateZoomLevel(p.m, p.c, z)
needProcess[zoomproc{p.c.Parent(), p.m}] = struct{}{}
}
}
log.Println(greq)
json.NewEncoder(rw).Encode(greq)
}
/*
func (a *App) mapdataIndex(rw http.ResponseWriter, req *http.Request) {
err := a.db.View(func(tx *bbolt.Tx) error {
grids := tx.Bucket([]byte("grids"))
if grids == nil {
return fmt.Errorf("grid not found")
}
return grids.ForEach(func(k, v []byte) error {
cur := GridData{}
err := json.Unmarshal(v, &cur)
if err != nil {
return err
}
fmt.Fprintf(rw, "%s,%d,%d,%d\n", cur.ID, cur.Map, cur.Coord.X, cur.Coord.Y)
return nil
})
})
if err != nil {
rw.WriteHeader(404)
}
}
*/
type ExtraData struct {
Season int
}
func (a *App) gridUpload(rw http.ResponseWriter, req *http.Request) {
if strings.Count(req.Header.Get("Content-Type"), "=") >= 2 && strings.Count(req.Header.Get("Content-Type"), "\"") == 0 {
parts := strings.SplitN(req.Header.Get("Content-Type"), "=", 2)
req.Header.Set("Content-Type", parts[0]+"=\""+parts[1]+"\"")
}
err := req.ParseMultipartForm(100000000)
if err != nil {
log.Println(err)
return
}
id := req.FormValue("id")
extraData := req.FormValue("extraData")
if extraData != "" {
ed := ExtraData{}
json.Unmarshal([]byte(extraData), &ed)
if ed.Season == 3 {
needTile := false
a.db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
curRaw := b.Get([]byte(id))
if curRaw == nil {
return fmt.Errorf("Unknown grid id: %s", id)
}
cur := GridData{}
err = json.Unmarshal(curRaw, &cur)
if err != nil {
return err
}
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
if err != nil {
return err
}
maps, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(cur.Map)))
if err != nil {
return err
}
zooms, err := maps.CreateBucketIfNotExists([]byte("0"))
if err != nil {
return err
}
tdRaw := zooms.Get([]byte(cur.Coord.Name()))
if tdRaw == nil {
needTile = true
return nil
}
td := TileData{}
err = json.Unmarshal(tdRaw, &td)
if err != nil {
return err
}
if td.File == "" {
needTile = true
return nil
}
if time.Now().After(cur.NextUpdate) {
cur.NextUpdate = time.Now().Add(time.Minute * 30)
}
raw, err := json.Marshal(cur)
if err != nil {
return err
}
b.Put([]byte(id), raw)
return nil
})
if !needTile {
log.Println("ignoring tile upload: winter")
return
} else {
log.Println("Missing tile, using winter version")
}
}
}
file, _, err := req.FormFile("file")
if err != nil {
log.Println(err)
return
}
log.Println("map tile for ", id)
updateTile := false
cur := GridData{}
mapid := 0
a.db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
curRaw := b.Get([]byte(id))
if curRaw == nil {
return fmt.Errorf("Unknown grid id: %s", id)
}
err = json.Unmarshal(curRaw, &cur)
if err != nil {
return err
}
updateTile = time.Now().After(cur.NextUpdate)
mapid = cur.Map
if updateTile {
cur.NextUpdate = time.Now().Add(time.Minute * 30)
}
raw, err := json.Marshal(cur)
if err != nil {
return err
}
b.Put([]byte(id), raw)
return nil
})
if updateTile {
os.MkdirAll(fmt.Sprintf("%s/grids", a.gridStorage), 0600)
f, err := os.Create(fmt.Sprintf("%s/grids/%s.png", a.gridStorage, cur.ID))
if err != nil {
return
}
_, err = io.Copy(f, file)
if err != nil {
f.Close()
return
}
f.Close()
a.SaveTile(mapid, cur.Coord, 0, fmt.Sprintf("grids/%s.png", cur.ID), time.Now().UnixNano())
c := cur.Coord
for z := 1; z <= 5; z++ {
c = c.Parent()
a.updateZoomLevel(mapid, c, z)
}
}
}
func (a *App) updateZoomLevel(mapid int, c Coord, z int) {
img := image.NewNRGBA(image.Rect(0, 0, 100, 100))
draw.Draw(img, img.Bounds(), image.Transparent, image.Point{}, draw.Src)
for x := 0; x <= 1; x++ {
for y := 0; y <= 1; y++ {
subC := c
subC.X *= 2
subC.Y *= 2
subC.X += x
subC.Y += y
td := a.GetTile(mapid, subC, z-1)
if td == nil || td.File == "" {
continue
}
subf, err := os.Open(filepath.Join(a.gridStorage, td.File))
if err != nil {
continue
}
subimg, _, err := image.Decode(subf)
subf.Close()
if err != nil {
continue
}
draw.BiLinear.Scale(img, image.Rect(50*x, 50*y, 50*x+50, 50*y+50), subimg, subimg.Bounds(), draw.Src, nil)
}
}
os.MkdirAll(fmt.Sprintf("%s/%d/%d", a.gridStorage, mapid, z), 0600)
f, err := os.Create(fmt.Sprintf("%s/%d/%d/%s.png", a.gridStorage, mapid, z, c.Name()))
a.SaveTile(mapid, c, z, fmt.Sprintf("%d/%d/%s.png", mapid, z, c.Name()), time.Now().UnixNano())
if err != nil {
return
}
defer func() {
f.Close()
}()
png.Encode(f, img)
}

437
internal/app/client_grid.go Normal file
View File

@@ -0,0 +1,437 @@
package app
import (
"encoding/json"
"fmt"
"image"
"image/png"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"go.etcd.io/bbolt"
"golang.org/x/image/draw"
)
type GridUpdate struct {
Grids [][]string `json:"grids"`
}
type GridRequest struct {
GridRequests []string `json:"gridRequests"`
Map int `json:"map"`
Coords Coord `json:"coords"`
}
type ExtraData struct {
Season int
}
func (a *App) gridUpdate(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
dec := json.NewDecoder(req.Body)
grup := GridUpdate{}
err := dec.Decode(&grup)
if err != nil {
log.Println("Error decoding grid request json: ", err)
http.Error(rw, "Error decoding request", http.StatusBadRequest)
return
}
log.Println(grup)
ops := []struct {
mapid int
x, y int
f string
}{}
greq := GridRequest{}
err = a.db.Update(func(tx *bbolt.Tx) error {
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
if err != nil {
return err
}
mapB, err := tx.CreateBucketIfNotExists([]byte("maps"))
if err != nil {
return err
}
configb, err := tx.CreateBucketIfNotExists([]byte("config"))
if err != nil {
return err
}
maps := map[int]struct{ X, Y int }{}
for x, row := range grup.Grids {
for y, grid := range row {
gridRaw := grids.Get([]byte(grid))
if gridRaw != nil {
gd := GridData{}
json.Unmarshal(gridRaw, &gd)
maps[gd.Map] = struct{ X, Y int }{gd.Coord.X - x, gd.Coord.Y - y}
}
}
}
if len(maps) == 0 {
seq, err := mapB.NextSequence()
if err != nil {
return err
}
mi := MapInfo{
ID: int(seq),
Name: strconv.Itoa(int(seq)),
Hidden: configb.Get([]byte("defaultHide")) != nil,
}
raw, _ := json.Marshal(mi)
err = mapB.Put([]byte(strconv.Itoa(int(seq))), raw)
if err != nil {
return err
}
log.Println("Client made mapid ", seq)
for x, row := range grup.Grids {
for y, grid := range row {
cur := GridData{}
cur.ID = grid
cur.Map = int(seq)
cur.Coord.X = x - 1
cur.Coord.Y = y - 1
raw, err := json.Marshal(cur)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
greq.GridRequests = append(greq.GridRequests, grid)
}
}
greq.Coords = Coord{0, 0}
return nil
}
mapid := -1
offset := struct{ X, Y int }{}
for id, off := range maps {
mi := MapInfo{}
mraw := mapB.Get([]byte(strconv.Itoa(id)))
if mraw != nil {
json.Unmarshal(mraw, &mi)
}
if mi.Priority {
mapid = id
offset = off
break
}
if id < mapid || mapid == -1 {
mapid = id
offset = off
}
}
log.Println("Client in mapid ", mapid)
for x, row := range grup.Grids {
for y, grid := range row {
cur := GridData{}
if curRaw := grids.Get([]byte(grid)); curRaw != nil {
json.Unmarshal(curRaw, &cur)
if time.Now().After(cur.NextUpdate) {
greq.GridRequests = append(greq.GridRequests, grid)
}
continue
}
cur.ID = grid
cur.Map = mapid
cur.Coord.X = x + offset.X
cur.Coord.Y = y + offset.Y
raw, err := json.Marshal(cur)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
greq.GridRequests = append(greq.GridRequests, grid)
}
}
if len(grup.Grids) >= 2 && len(grup.Grids[1]) >= 2 {
if curRaw := grids.Get([]byte(grup.Grids[1][1])); curRaw != nil {
cur := GridData{}
json.Unmarshal(curRaw, &cur)
greq.Map = cur.Map
greq.Coords = cur.Coord
}
}
if len(maps) > 1 {
grids.ForEach(func(k, v []byte) error {
gd := GridData{}
json.Unmarshal(v, &gd)
if gd.Map == mapid {
return nil
}
if merge, ok := maps[gd.Map]; ok {
var td *TileData
mapb, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(gd.Map)))
if err != nil {
return err
}
zoom, err := mapb.CreateBucketIfNotExists([]byte(strconv.Itoa(0)))
if err != nil {
return err
}
tileraw := zoom.Get([]byte(gd.Coord.Name()))
if tileraw != nil {
json.Unmarshal(tileraw, &td)
}
gd.Map = mapid
gd.Coord.X += offset.X - merge.X
gd.Coord.Y += offset.Y - merge.Y
raw, _ := json.Marshal(gd)
if td != nil {
ops = append(ops, struct {
mapid int
x int
y int
f string
}{
mapid: mapid,
x: gd.Coord.X,
y: gd.Coord.Y,
f: td.File,
})
}
grids.Put(k, raw)
}
return nil
})
}
for mergeid, merge := range maps {
if mapid == mergeid {
continue
}
mapB.Delete([]byte(strconv.Itoa(mergeid)))
log.Println("Reporting merge", mergeid, mapid)
a.reportMerge(mergeid, mapid, Coord{X: offset.X - merge.X, Y: offset.Y - merge.Y})
}
return nil
})
if err != nil {
log.Println(err)
return
}
needProcess := map[zoomproc]struct{}{}
for _, op := range ops {
a.SaveTile(op.mapid, Coord{X: op.x, Y: op.y}, 0, op.f, time.Now().UnixNano())
needProcess[zoomproc{c: Coord{X: op.x, Y: op.y}.Parent(), m: op.mapid}] = struct{}{}
}
for z := 1; z <= 5; z++ {
process := needProcess
needProcess = map[zoomproc]struct{}{}
for p := range process {
a.updateZoomLevel(p.m, p.c, z)
needProcess[zoomproc{p.c.Parent(), p.m}] = struct{}{}
}
}
log.Println(greq)
json.NewEncoder(rw).Encode(greq)
}
func (a *App) gridUpload(rw http.ResponseWriter, req *http.Request) {
if strings.Count(req.Header.Get("Content-Type"), "=") >= 2 && strings.Count(req.Header.Get("Content-Type"), "\"") == 0 {
parts := strings.SplitN(req.Header.Get("Content-Type"), "=", 2)
req.Header.Set("Content-Type", parts[0]+"=\""+parts[1]+"\"")
}
err := req.ParseMultipartForm(100000000)
if err != nil {
log.Println(err)
return
}
id := req.FormValue("id")
extraData := req.FormValue("extraData")
if extraData != "" {
ed := ExtraData{}
json.Unmarshal([]byte(extraData), &ed)
if ed.Season == 3 {
needTile := false
a.db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
curRaw := b.Get([]byte(id))
if curRaw == nil {
return fmt.Errorf("Unknown grid id: %s", id)
}
cur := GridData{}
err = json.Unmarshal(curRaw, &cur)
if err != nil {
return err
}
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
if err != nil {
return err
}
maps, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(cur.Map)))
if err != nil {
return err
}
zooms, err := maps.CreateBucketIfNotExists([]byte("0"))
if err != nil {
return err
}
tdRaw := zooms.Get([]byte(cur.Coord.Name()))
if tdRaw == nil {
needTile = true
return nil
}
td := TileData{}
err = json.Unmarshal(tdRaw, &td)
if err != nil {
return err
}
if td.File == "" {
needTile = true
return nil
}
if time.Now().After(cur.NextUpdate) {
cur.NextUpdate = time.Now().Add(time.Minute * 30)
}
raw, err := json.Marshal(cur)
if err != nil {
return err
}
b.Put([]byte(id), raw)
return nil
})
if !needTile {
log.Println("ignoring tile upload: winter")
return
} else {
log.Println("Missing tile, using winter version")
}
}
}
file, _, err := req.FormFile("file")
if err != nil {
log.Println(err)
return
}
log.Println("map tile for ", id)
updateTile := false
cur := GridData{}
mapid := 0
a.db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
if err != nil {
return err
}
curRaw := b.Get([]byte(id))
if curRaw == nil {
return fmt.Errorf("Unknown grid id: %s", id)
}
err = json.Unmarshal(curRaw, &cur)
if err != nil {
return err
}
updateTile = time.Now().After(cur.NextUpdate)
mapid = cur.Map
if updateTile {
cur.NextUpdate = time.Now().Add(time.Minute * 30)
}
raw, err := json.Marshal(cur)
if err != nil {
return err
}
b.Put([]byte(id), raw)
return nil
})
if updateTile {
os.MkdirAll(fmt.Sprintf("%s/grids", a.gridStorage), 0600)
f, err := os.Create(fmt.Sprintf("%s/grids/%s.png", a.gridStorage, cur.ID))
if err != nil {
return
}
_, err = io.Copy(f, file)
if err != nil {
f.Close()
return
}
f.Close()
a.SaveTile(mapid, cur.Coord, 0, fmt.Sprintf("grids/%s.png", cur.ID), time.Now().UnixNano())
c := cur.Coord
for z := 1; z <= 5; z++ {
c = c.Parent()
a.updateZoomLevel(mapid, c, z)
}
}
}
func (a *App) updateZoomLevel(mapid int, c Coord, z int) {
img := image.NewNRGBA(image.Rect(0, 0, 100, 100))
draw.Draw(img, img.Bounds(), image.Transparent, image.Point{}, draw.Src)
for x := 0; x <= 1; x++ {
for y := 0; y <= 1; y++ {
subC := c
subC.X *= 2
subC.Y *= 2
subC.X += x
subC.Y += y
td := a.GetTile(mapid, subC, z-1)
if td == nil || td.File == "" {
continue
}
subf, err := os.Open(filepath.Join(a.gridStorage, td.File))
if err != nil {
continue
}
subimg, _, err := image.Decode(subf)
subf.Close()
if err != nil {
continue
}
draw.BiLinear.Scale(img, image.Rect(50*x, 50*y, 50*x+50, 50*y+50), subimg, subimg.Bounds(), draw.Src, nil)
}
}
os.MkdirAll(fmt.Sprintf("%s/%d/%d", a.gridStorage, mapid, z), 0600)
f, err := os.Create(fmt.Sprintf("%s/%d/%d/%s.png", a.gridStorage, mapid, z, c.Name()))
a.SaveTile(mapid, c, z, fmt.Sprintf("%d/%d/%s.png", mapid, z, c.Name()), time.Now().UnixNano())
if err != nil {
return
}
defer func() {
f.Close()
}()
png.Encode(f, img)
}

View File

@@ -0,0 +1,82 @@
package app
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"go.etcd.io/bbolt"
)
func (a *App) uploadMarkers(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
markers := []struct {
Name string
GridID string
X, Y int
Image string
Type string
Color string
}{}
buf, err := io.ReadAll(req.Body)
if err != nil {
log.Println("Error reading marker json: ", err)
return
}
err = json.Unmarshal(buf, &markers)
if err != nil {
log.Println("Error decoding marker json: ", err)
log.Println("Original json: ", string(buf))
return
}
err = a.db.Update(func(tx *bbolt.Tx) error {
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
if err != nil {
return err
}
grid, err := mb.CreateBucketIfNotExists([]byte("grid"))
if err != nil {
return err
}
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
if err != nil {
return err
}
for _, mraw := range markers {
key := []byte(fmt.Sprintf("%s_%d_%d", mraw.GridID, mraw.X, mraw.Y))
if grid.Get(key) != nil {
continue
}
if mraw.Image == "" {
mraw.Image = "gfx/terobjs/mm/custom"
}
id, err := idB.NextSequence()
if err != nil {
return err
}
idKey := []byte(strconv.Itoa(int(id)))
m := Marker{
Name: mraw.Name,
ID: int(id),
GridID: mraw.GridID,
Position: Position{
X: mraw.X,
Y: mraw.Y,
},
Image: mraw.Image,
}
raw, _ := json.Marshal(m)
grid.Put(key, raw)
idB.Put(idKey, key)
}
return nil
})
if err != nil {
log.Println("Error update db: ", err)
return
}
}

View File

@@ -0,0 +1,97 @@
package app
import (
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"time"
"go.etcd.io/bbolt"
)
func (a *App) updatePositions(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
craws := map[string]struct {
Name string
GridID string
Coords struct {
X, Y int
}
Type string
}{}
buf, err := io.ReadAll(req.Body)
if err != nil {
log.Println("Error reading position update json: ", err)
return
}
err = json.Unmarshal(buf, &craws)
if err != nil {
log.Println("Error decoding position update json: ", err)
log.Println("Original json: ", string(buf))
return
}
// Read grid data first (inside db.View), then update characters (with chmu only).
// Avoid holding db.View and chmu simultaneously to prevent deadlock.
gridDataByID := make(map[string]GridData)
a.db.View(func(tx *bbolt.Tx) error {
grids := tx.Bucket([]byte("grids"))
if grids == nil {
return nil
}
for _, craw := range craws {
grid := grids.Get([]byte(craw.GridID))
if grid != nil {
var gd GridData
if json.Unmarshal(grid, &gd) == nil {
gridDataByID[craw.GridID] = gd
}
}
}
return nil
})
a.chmu.Lock()
defer a.chmu.Unlock()
for id, craw := range craws {
gd, ok := gridDataByID[craw.GridID]
if !ok {
continue
}
idnum, _ := strconv.Atoi(id)
c := Character{
Name: craw.Name,
ID: idnum,
Map: gd.Map,
Position: Position{
X: craw.Coords.X + (gd.Coord.X * 100),
Y: craw.Coords.Y + (gd.Coord.Y * 100),
},
Type: craw.Type,
updated: time.Now(),
}
old, ok := a.characters[id]
if !ok {
a.characters[id] = c
} else {
if old.Type == "player" {
if c.Type == "player" {
a.characters[id] = c
} else {
old.Position = c.Position
a.characters[id] = old
}
} else if old.Type != "unknown" {
if c.Type != "unknown" {
a.characters[id] = c
} else {
old.Position = c.Position
a.characters[id] = old
}
} else {
a.characters[id] = c
}
}
}
}

View File

@@ -0,0 +1,41 @@
package app
import (
"net/http"
)
func (a *App) redirectRoot(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
http.NotFound(rw, req)
return
}
if a.setupRequired() {
http.Redirect(rw, req, "/map/setup", http.StatusFound)
return
}
http.Redirect(rw, req, "/map/profile", http.StatusFound)
}
func (a *App) redirectLogin(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/login" {
http.NotFound(rw, req)
return
}
http.Redirect(rw, req, "/map/login", http.StatusFound)
}
func (a *App) redirectLogout(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/logout" {
http.NotFound(rw, req)
return
}
s := a.getSession(req)
if s != nil {
a.deleteSession(s)
}
http.Redirect(rw, req, "/map/login", http.StatusFound)
}
func (a *App) redirectAdmin(rw http.ResponseWriter, req *http.Request) {
http.Redirect(rw, req, "/map/admin", http.StatusFound)
}

View File

@@ -1,175 +0,0 @@
package app
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"time"
"go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
func (a *App) index(rw http.ResponseWriter, req *http.Request) {
s := a.getSession(req)
if s == nil {
http.Redirect(rw, req, "/login", 302)
return
}
tokens := []string{}
prefix := "http://example.com"
a.db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte("users"))
if b == nil {
return nil
}
uRaw := b.Get([]byte(s.Username))
if uRaw == nil {
return nil
}
u := User{}
json.Unmarshal(uRaw, &u)
tokens = u.Tokens
config := tx.Bucket([]byte("config"))
if config != nil {
prefix = string(config.Get([]byte("prefix")))
}
return nil
})
a.ExecuteTemplate(rw, "index.tmpl", struct {
Page Page
Session *Session
UploadTokens []string
Prefix string
}{
Page: a.getPage(req),
Session: s,
UploadTokens: tokens,
Prefix: prefix,
})
}
func (a *App) login(rw http.ResponseWriter, req *http.Request) {
if req.Method == "POST" {
u := a.getUser(req.FormValue("user"), req.FormValue("pass"))
if u != nil {
session := make([]byte, 32)
rand.Read(session)
http.SetCookie(rw, &http.Cookie{
Name: "session",
Expires: time.Now().Add(time.Hour * 24 * 7),
Value: hex.EncodeToString(session),
})
s := &Session{
ID: hex.EncodeToString(session),
Username: req.FormValue("user"),
TempAdmin: u.Auths.Has("tempadmin"),
}
a.saveSession(s)
http.Redirect(rw, req, "/", 302)
return
}
}
a.ExecuteTemplate(rw, "login.tmpl", struct {
Page Page
}{
Page: a.getPage(req),
})
}
func (a *App) logout(rw http.ResponseWriter, req *http.Request) {
s := a.getSession(req)
if s != nil {
a.deleteSession(s)
}
http.Redirect(rw, req, "/login", 302)
return
}
func (a *App) generateToken(rw http.ResponseWriter, req *http.Request) {
s := a.getSession(req)
if s == nil || !s.Auths.Has(AUTH_UPLOAD) {
http.Redirect(rw, req, "/", 302)
return
}
tokenRaw := make([]byte, 16)
_, err := rand.Read(tokenRaw)
if err != nil {
rw.WriteHeader(500)
return
}
token := hex.EncodeToString(tokenRaw)
a.db.Update(func(tx *bbolt.Tx) error {
ub, err := tx.CreateBucketIfNotExists([]byte("users"))
if err != nil {
return err
}
uRaw := ub.Get([]byte(s.Username))
if uRaw == nil {
return nil
}
u := User{}
err = json.Unmarshal(uRaw, &u)
if err != nil {
return err
}
u.Tokens = append(u.Tokens, token)
buf, err := json.Marshal(u)
if err != nil {
return err
}
err = ub.Put([]byte(s.Username), buf)
if err != nil {
return err
}
b, err := tx.CreateBucketIfNotExists([]byte("tokens"))
if err != nil {
return err
}
return b.Put([]byte(token), []byte(s.Username))
})
http.Redirect(rw, req, "/", 302)
}
func (a *App) changePassword(rw http.ResponseWriter, req *http.Request) {
s := a.getSession(req)
if s == nil {
http.Redirect(rw, req, "/", 302)
return
}
if req.Method == "POST" {
req.ParseForm()
password := req.FormValue("pass")
a.db.Update(func(tx *bbolt.Tx) error {
users, err := tx.CreateBucketIfNotExists([]byte("users"))
if err != nil {
return err
}
u := User{}
raw := users.Get([]byte(s.Username))
if raw != nil {
json.Unmarshal(raw, &u)
}
if password != "" {
u.Pass, _ = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}
raw, _ = json.Marshal(u)
users.Put([]byte(s.Username), raw)
return nil
})
http.Redirect(rw, req, "/", 302)
}
a.ExecuteTemplate(rw, "password.tmpl", struct {
Page Page
Session *Session
}{
Page: a.getPage(req),
Session: s,
})
}