From a0290f6a6d840e113036a9fb5e013ad39c6c5978 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Tue, 23 Dec 2025 12:23:25 +0100 Subject: [PATCH] add data models --- model/point.go | 25 ++++++ model/session.go | 128 ++++++++++++++++++++++++++++++ model/share.go | 203 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 model/point.go create mode 100644 model/session.go create mode 100644 model/share.go diff --git a/model/point.go b/model/point.go new file mode 100644 index 0000000..d6bc118 --- /dev/null +++ b/model/point.go @@ -0,0 +1,25 @@ +package model + +type Point struct { + IV string `json:"iv,omitempty"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + Time float64 `json:"time"` + Provider int `json:"prv"` + Accuracy *float64 `json:"acc,omitempty"` + Speed *float64 `json:"spd,omitempty"` +} + +func (p Point) ToArray(encrypted bool) []any { + if encrypted { + return []any{p.IV, p.Lat, p.Lon, p.Time, p.Provider, p.Accuracy, p.Speed} + } + return []any{p.Lat, p.Lon, p.Time, p.Provider, p.Accuracy, p.Speed} +} + +func (p Point) TimeIndex(encrypted bool) int { + if encrypted { + return 3 + } + return 2 +} diff --git a/model/session.go b/model/session.go new file mode 100644 index 0000000..cb1f6a3 --- /dev/null +++ b/model/session.go @@ -0,0 +1,128 @@ +package model + +import ( + "context" + "crypto/rand" + "encoding/hex" + "time" + + "github.com/parkan/go-hauk/store" +) + +const ( + PrefixSession = "session-" + SessionIDSize = 32 +) + +type SessionData struct { + Expire int64 `json:"expire"` + Interval float64 `json:"interval"` + Targets []string `json:"targets"` + Points [][]any `json:"points"` + Encrypted bool `json:"encrypted"` + Salt string `json:"salt,omitempty"` +} + +type Session struct { + store store.Store + id string + data SessionData + maxPts int +} + +func NewSession(s store.Store, maxPts int) (*Session, error) { + id, err := generateSessionID() + if err != nil { + return nil, err + } + return &Session{ + store: s, + id: id, + maxPts: maxPts, + data: SessionData{ + Targets: []string{}, + Points: [][]any{}, + }, + }, nil +} + +func LoadSession(ctx context.Context, s store.Store, id string, maxPts int) (*Session, error) { + sess := &Session{store: s, id: id, maxPts: maxPts} + err := s.Get(ctx, PrefixSession+id, &sess.data) + if err != nil { + return nil, err + } + return sess, nil +} + +func (s *Session) ID() string { return s.id } +func (s *Session) Expire() time.Time { return time.Unix(s.data.Expire, 0) } +func (s *Session) Interval() float64 { return s.data.Interval } +func (s *Session) Targets() []string { return s.data.Targets } +func (s *Session) Points() [][]any { return s.data.Points } +func (s *Session) Encrypted() bool { return s.data.Encrypted } +func (s *Session) Salt() string { return s.data.Salt } +func (s *Session) HasExpired() bool { return time.Now().Unix() >= s.data.Expire } + +func (s *Session) SetExpire(t time.Time) { s.data.Expire = t.Unix() } +func (s *Session) SetInterval(i float64) { s.data.Interval = i } +func (s *Session) SetEncrypted(e bool, salt string) { + s.data.Encrypted = e + s.data.Salt = salt +} + +func (s *Session) AddTarget(shareID string) { + s.data.Targets = append(s.data.Targets, shareID) +} + +func (s *Session) RemoveTarget(shareID string) { + for i, t := range s.data.Targets { + if t == shareID { + s.data.Targets = append(s.data.Targets[:i], s.data.Targets[i+1:]...) + return + } + } +} + +func (s *Session) AddPoint(p []any) { + s.data.Points = append(s.data.Points, p) + if len(s.data.Points) > s.maxPts { + s.data.Points = s.data.Points[len(s.data.Points)-s.maxPts:] + } +} + +func (s *Session) GetPoints(since float64) [][]any { + if since <= 0 { + return s.data.Points + } + timeIdx := 2 + if s.data.Encrypted { + timeIdx = 3 + } + var pts [][]any + for _, p := range s.data.Points { + if len(p) > timeIdx { + if t, ok := p[timeIdx].(float64); ok && t > since { + pts = append(pts, p) + } + } + } + return pts +} + +func (s *Session) Save(ctx context.Context) error { + return s.store.Set(ctx, PrefixSession+s.id, s.data, s.Expire()) +} + +func (s *Session) Delete(ctx context.Context) error { + return s.store.Delete(ctx, PrefixSession+s.id) +} + +func generateSessionID() (string, error) { + b := make([]byte, SessionIDSize) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/model/share.go b/model/share.go new file mode 100644 index 0000000..14a9783 --- /dev/null +++ b/model/share.go @@ -0,0 +1,203 @@ +package model + +import ( + "context" + "math/rand" + "strconv" + "time" + + "github.com/parkan/go-hauk/store" +) + +const ( + PrefixLocdata = "locdata-" + PrefixGroupID = "groupid-" + + ShareTypeAlone = 0 + ShareTypeGroup = 1 + + GroupPinMin = 100000 + GroupPinMax = 999999 +) + +type SoloShareData struct { + Type int `json:"type"` + Expire int64 `json:"expire"` + Host string `json:"host"` + Adoptable bool `json:"adoptable"` +} + +type GroupShareData struct { + Type int `json:"type"` + Expire int64 `json:"expire"` + Hosts map[string]string `json:"hosts"` + GroupPin int `json:"groupPin"` +} + +type SoloShare struct { + store store.Store + id string + data SoloShareData + publicURL string +} + +type GroupShare struct { + store store.Store + id string + data GroupShareData + publicURL string +} + +func NewSoloShare(s store.Store, publicURL string, linkGen func() (string, error)) (*SoloShare, error) { + id, err := linkGen() + if err != nil { + return nil, err + } + return &SoloShare{ + store: s, + id: id, + publicURL: publicURL, + data: SoloShareData{ + Type: ShareTypeAlone, + }, + }, nil +} + +func LoadSoloShare(ctx context.Context, s store.Store, id, publicURL string) (*SoloShare, error) { + share := &SoloShare{store: s, id: id, publicURL: publicURL} + err := s.Get(ctx, PrefixLocdata+id, &share.data) + if err != nil { + return nil, err + } + return share, nil +} + +func (s *SoloShare) ID() string { return s.id } +func (s *SoloShare) Type() int { return s.data.Type } +func (s *SoloShare) Expire() time.Time { return time.Unix(s.data.Expire, 0) } +func (s *SoloShare) Host() string { return s.data.Host } +func (s *SoloShare) Adoptable() bool { return s.data.Adoptable } +func (s *SoloShare) ViewLink() string { return s.publicURL + "?" + s.id } + +func (s *SoloShare) SetExpire(t time.Time) { s.data.Expire = t.Unix() } +func (s *SoloShare) SetHost(sid string) { s.data.Host = sid } +func (s *SoloShare) SetAdoptable(a bool) { s.data.Adoptable = a } +func (s *SoloShare) SetID(id string) { s.id = id } + +func (s *SoloShare) Save(ctx context.Context) error { + return s.store.Set(ctx, PrefixLocdata+s.id, s.data, s.Expire()) +} + +func (s *SoloShare) Delete(ctx context.Context) error { + return s.store.Delete(ctx, PrefixLocdata+s.id) +} + +func NewGroupShare(s store.Store, publicURL string, linkGen func() (string, error)) (*GroupShare, error) { + id, err := linkGen() + if err != nil { + return nil, err + } + pin := GroupPinMin + rand.Intn(GroupPinMax-GroupPinMin+1) + return &GroupShare{ + store: s, + id: id, + publicURL: publicURL, + data: GroupShareData{ + Type: ShareTypeGroup, + Hosts: make(map[string]string), + GroupPin: pin, + }, + }, nil +} + +func LoadGroupShare(ctx context.Context, s store.Store, id, publicURL string) (*GroupShare, error) { + share := &GroupShare{store: s, id: id, publicURL: publicURL} + err := s.Get(ctx, PrefixLocdata+id, &share.data) + if err != nil { + return nil, err + } + return share, nil +} + +func LoadGroupShareByPin(ctx context.Context, s store.Store, pin int, publicURL string) (*GroupShare, error) { + var shareID string + err := s.Get(ctx, PrefixGroupID+strconv.Itoa(pin), &shareID) + if err != nil { + return nil, err + } + return LoadGroupShare(ctx, s, shareID, publicURL) +} + +func (g *GroupShare) ID() string { return g.id } +func (g *GroupShare) Type() int { return g.data.Type } +func (g *GroupShare) Expire() time.Time { return time.Unix(g.data.Expire, 0) } +func (g *GroupShare) Hosts() map[string]string { return g.data.Hosts } +func (g *GroupShare) Pin() int { return g.data.GroupPin } +func (g *GroupShare) ViewLink() string { return g.publicURL + "?" + g.id } + +func (g *GroupShare) SetExpire(t time.Time) { g.data.Expire = t.Unix() } +func (g *GroupShare) SetID(id string) { g.id = id } + +func (g *GroupShare) AddHost(nick, sessionID string) { + g.data.Hosts[nick] = sessionID +} + +func (g *GroupShare) RemoveHost(sessionID string) { + for nick, sid := range g.data.Hosts { + if sid == sessionID { + delete(g.data.Hosts, nick) + return + } + } +} + +func (g *GroupShare) Save(ctx context.Context) error { + if err := g.store.Set(ctx, PrefixLocdata+g.id, g.data, g.Expire()); err != nil { + return err + } + return g.store.Set(ctx, PrefixGroupID+strconv.Itoa(g.data.GroupPin), g.id, g.Expire()) +} + +func (g *GroupShare) Delete(ctx context.Context) error { + g.store.Delete(ctx, PrefixGroupID+strconv.Itoa(g.data.GroupPin)) + return g.store.Delete(ctx, PrefixLocdata+g.id) +} + +func (g *GroupShare) GetAllPoints(ctx context.Context, since float64, maxPts int) (map[string][][]any, error) { + points := make(map[string][][]any) + for nick, sid := range g.data.Hosts { + sess, err := LoadSession(ctx, g.store, sid, maxPts) + if err != nil { + continue + } + points[nick] = sess.GetPoints(since) + } + return points, nil +} + +func (g *GroupShare) GetAutoInterval(ctx context.Context, maxPts int) float64 { + min := float64(0) + for _, sid := range g.data.Hosts { + sess, err := LoadSession(ctx, g.store, sid, maxPts) + if err != nil { + continue + } + if min == 0 || sess.Interval() < min { + min = sess.Interval() + } + } + return min +} + +type ShareType struct { + Type int `json:"type"` +} + +func LoadShareType(ctx context.Context, s store.Store, id string) (int, error) { + var st ShareType + err := s.Get(ctx, PrefixLocdata+id, &st) + if err != nil { + return -1, err + } + return st.Type, nil +}