diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 0144264b..10d8563a 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -47,6 +47,7 @@ func (api *ApiManagerCtx) Route(r types.Router) { r.Post("/logout", api.Logout) r.Get("/whoami", api.Whoami) r.Post("/profile", api.UpdateProfile) + r.Get("/stats", api.Stats) sessionsHandler := sessions.New(api.sessions) r.Route("/sessions", sessionsHandler.Route) diff --git a/server/internal/api/session.go b/server/internal/api/session.go index 358521be..1ed1ad63 100644 --- a/server/internal/api/session.go +++ b/server/internal/api/session.go @@ -103,3 +103,8 @@ func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request) return utils.HttpSuccess(w, true) } + +func (api *ApiManagerCtx) Stats(w http.ResponseWriter, r *http.Request) error { + stats := api.sessions.Stats() + return utils.HttpSuccess(w, stats) +} diff --git a/server/internal/http/legacy/handler.go b/server/internal/http/legacy/handler.go index b61517c7..172fef1a 100644 --- a/server/internal/http/legacy/handler.go +++ b/server/internal/http/legacy/handler.go @@ -7,10 +7,8 @@ import ( "io" "net/http" "net/url" - "time" "m1k1o/neko/internal/api" - "m1k1o/neko/internal/api/room" oldEvent "m1k1o/neko/internal/http/legacy/event" oldMessage "m1k1o/neko/internal/http/legacy/message" oldTypes "m1k1o/neko/internal/http/legacy/types" @@ -43,7 +41,6 @@ var ( type LegacyHandler struct { logger zerolog.Logger serverAddr string - startedAt time.Time } func New() *LegacyHandler { @@ -52,7 +49,6 @@ func New() *LegacyHandler { return &LegacyHandler{ logger: log.With().Str("module", "legacy").Logger(), serverAddr: "127.0.0.1:8080", - startedAt: time.Now(), } } @@ -208,9 +204,9 @@ func (h *LegacyHandler) Route(r types.Router) { return utils.HttpInternalServerError().WithInternalErr(err) } - // get current control status - control := room.ControlStatusPayload{} - err = s.apiReq(http.MethodGet, "/api/room/control", nil, &control) + // get stats + newStats := types.Stats{} + err = s.apiReq(http.MethodGet, "/api/stats", nil, &newStats) if err != nil { return utils.HttpInternalServerError().WithInternalErr(err) } @@ -236,18 +232,6 @@ func (h *LegacyHandler) Route(r types.Router) { } // append members stats.Members = append(stats.Members, member) - } else if session.State.NotConnectedSince != nil { - // - // TODO: This wont work if the user is removed after the session is closed - // - // populate last admin left time - if session.Profile.IsAdmin && (stats.LastAdminLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastAdminLeftAt)) { - stats.LastAdminLeftAt = session.State.NotConnectedSince - } - // populate last user left time - if !session.Profile.IsAdmin && (stats.LastUserLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastUserLeftAt)) { - stats.LastUserLeftAt = session.State.NotConnectedSince - } } } @@ -256,10 +240,12 @@ func (h *LegacyHandler) Route(r types.Router) { return err } - stats.Host = control.HostId + stats.Host = newStats.HostId // TODO: stats.Banned, not implemented yet stats.Locked = locks - stats.ServerStartedAt = h.startedAt + stats.ServerStartedAt = newStats.ServerStartedAt + stats.LastAdminLeftAt = newStats.LastAdminLeftAt + stats.LastUserLeftAt = newStats.LastUserLeftAt stats.ControlProtection = settings.ControlProtection stats.ImplicitControl = settings.ImplicitHosting diff --git a/server/internal/session/manager.go b/server/internal/session/manager.go index aa0d63b5..29bb8e4b 100644 --- a/server/internal/session/manager.go +++ b/server/internal/session/manager.go @@ -4,6 +4,7 @@ import ( "errors" "sync" "sync/atomic" + "time" "github.com/kataras/go-events" "github.com/rs/zerolog" @@ -31,6 +32,8 @@ func New(config *config.Session) *SessionManagerCtx { sessions: make(map[string]*SessionCtx), cursors: make(map[types.Session][]types.Cursor), emmiter: events.New(), + + serverStartedAt: time.Now(), } // create API session @@ -76,6 +79,12 @@ type SessionManagerCtx struct { emmiter events.EventEmmiter apiSession *SessionCtx + + serverStartedAt time.Time + totalAdmins atomic.Int32 + lastAdminLeftAt atomic.Value + totalUsers atomic.Int32 + lastUserLeftAt atomic.Value } func (manager *SessionManagerCtx) Create(id string, profile types.MemberProfile) (types.Session, string, error) { @@ -468,3 +477,36 @@ func (manager *SessionManagerCtx) Settings() types.Settings { func (manager *SessionManagerCtx) CookieEnabled() bool { return manager.config.CookieEnabled } + +// --- +// stats +// --- + +func (manager *SessionManagerCtx) Stats() types.Stats { + hostId := "" + + host, hasHost := manager.GetHost() + if hasHost { + hostId = host.ID() + } + + var lastUserLeftAt *time.Time + if t, ok := manager.lastUserLeftAt.Load().(*time.Time); ok { + lastUserLeftAt = t + } + + var lastAdminLeftAt *time.Time + if t, ok := manager.lastAdminLeftAt.Load().(*time.Time); ok { + lastAdminLeftAt = t + } + + return types.Stats{ + HasHost: hasHost, + HostId: hostId, + ServerStartedAt: manager.serverStartedAt, + TotalUsers: int(manager.totalUsers.Load()), + LastUserLeftAt: lastUserLeftAt, + TotalAdmins: int(manager.totalAdmins.Load()), + LastAdminLeftAt: lastAdminLeftAt, + } +} diff --git a/server/internal/session/session.go b/server/internal/session/session.go index 93a6a220..769b42af 100644 --- a/server/internal/session/session.go +++ b/server/internal/session/session.go @@ -121,6 +121,14 @@ func (session *SessionCtx) ConnectWebSocketPeer(websocketPeer types.WebSocketPee session.state.ConnectedSince = &now session.state.NotConnectedSince = nil + if session.profile.IsAdmin { + session.manager.totalAdmins.Add(1) + session.manager.lastAdminLeftAt.Store((*time.Time)(nil)) + } else { + session.manager.totalUsers.Add(1) + session.manager.lastUserLeftAt.Store((*time.Time)(nil)) + } + session.manager.emmiter.Emit("connected", session) // if there is a previous peer, destroy it @@ -180,6 +188,16 @@ func (session *SessionCtx) DisconnectWebSocketPeer(websocketPeer types.WebSocket session.state.ConnectedSince = nil session.state.NotConnectedSince = &now + if session.profile.IsAdmin { + if session.manager.totalAdmins.Add(-1) == 0 { + session.manager.lastAdminLeftAt.Store(&now) + } + } else { + if session.manager.totalUsers.Add(-1) == 0 { + session.manager.lastUserLeftAt.Store(&now) + } + } + session.manager.emmiter.Emit("disconnected", session) session.websocketMu.Lock() diff --git a/server/openapi.yaml b/server/openapi.yaml index bf5e18c8..08adee68 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -125,6 +125,21 @@ paths: schema: $ref: '#/components/schemas/MemberProfile' required: true + /api/stats: + get: + summary: stats + operationId: stats + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Stats' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' # # sessions @@ -1145,6 +1160,30 @@ components: is_watching: type: boolean + Stats: + type: object + properties: + has_host: + type: boolean + host_id: + type: string + optional: true + server_started_at: + type: string + format: date-time + total_users: + type: integer + last_user_left_at: + type: string + format: date-time + optional: true + total_admins: + type: integer + last_admin_left_at: + type: string + format: date-time + optional: true + # # room # diff --git a/server/pkg/types/session.go b/server/pkg/types/session.go index e03ef235..fba64d8c 100644 --- a/server/pkg/types/session.go +++ b/server/pkg/types/session.go @@ -52,6 +52,16 @@ type Settings struct { Plugins PluginSettings `json:"plugins"` } +type Stats struct { + HasHost bool `json:"has_host"` + HostId string `json:"host_id,omitempty"` + ServerStartedAt time.Time `json:"server_started_at"` + TotalUsers int `json:"total_users"` + LastUserLeftAt *time.Time `json:"last_user_left_at,omitempty"` + TotalAdmins int `json:"total_admins"` + LastAdminLeftAt *time.Time `json:"last_admin_left_at,omitempty"` +} + type Session interface { ID() string Profile() MemberProfile @@ -110,6 +120,8 @@ type SessionManager interface { Settings() Settings CookieEnabled() bool + Stats() Stats + CookieSetToken(w http.ResponseWriter, token string) CookieClearToken(w http.ResponseWriter, r *http.Request) Authenticate(r *http.Request) (Session, error)