All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
119 lines
3.1 KiB
Go
119 lines
3.1 KiB
Go
package handlers
|
|
|
|
// presence.go — Heartbeat upsert and status query
|
|
//
|
|
// Clients POST /api/v1/presence/heartbeat every 30s while active.
|
|
// GET /api/v1/presence?users=id1,id2 returns current status.
|
|
// Online = last_seen within 90s.
|
|
//
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"armature/store"
|
|
)
|
|
|
|
const presenceOnlineThreshold = 90 * time.Second
|
|
|
|
type PresenceHandler struct {
|
|
stores store.Stores
|
|
}
|
|
|
|
func NewPresenceHandler(s store.Stores) *PresenceHandler {
|
|
return &PresenceHandler{stores: s}
|
|
}
|
|
|
|
// PresenceHeartbeat upserts the calling user's last_seen timestamp.
|
|
func (h *PresenceHandler) Heartbeat(c *gin.Context) {
|
|
userID := getUserID(c)
|
|
if err := h.stores.Presence.Heartbeat(c.Request.Context(), userID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "presence update failed"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// PresenceQuery returns online/offline status for a list of user IDs.
|
|
// Query param: ?users=uuid1,uuid2,...
|
|
func (h *PresenceHandler) Query(c *gin.Context) {
|
|
raw := c.Query("users")
|
|
if raw == "" {
|
|
c.JSON(http.StatusOK, gin.H{"presence": map[string]string{}})
|
|
return
|
|
}
|
|
ids := strings.Split(raw, ",")
|
|
if len(ids) > 100 {
|
|
ids = ids[:100]
|
|
}
|
|
|
|
// Trim whitespace
|
|
cleaned := make([]string, 0, len(ids))
|
|
for _, id := range ids {
|
|
id = strings.TrimSpace(id)
|
|
if id != "" {
|
|
cleaned = append(cleaned, id)
|
|
}
|
|
}
|
|
|
|
threshold := time.Now().Add(-presenceOnlineThreshold)
|
|
result, err := h.stores.Presence.GetStatuses(c.Request.Context(), cleaned, threshold)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "presence query failed"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"presence": result})
|
|
}
|
|
|
|
// SearchUsers returns a lightweight list of approved users matching a query.
|
|
// GET /api/v1/users/search?q=alice
|
|
func (h *PresenceHandler) SearchUsers(c *gin.Context) {
|
|
userID := getUserID(c)
|
|
q := strings.TrimSpace(c.Query("q"))
|
|
|
|
results, err := h.stores.Users.SearchActive(c.Request.Context(), userID, q)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "search failed"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": results})
|
|
}
|
|
|
|
// ResolveUsers returns identity records for a batch of user IDs.
|
|
// GET /api/v1/users/resolve?ids=uuid1,uuid2,...
|
|
func (h *PresenceHandler) ResolveUsers(c *gin.Context) {
|
|
raw := strings.TrimSpace(c.Query("ids"))
|
|
if raw == "" {
|
|
c.JSON(http.StatusOK, gin.H{"data": map[string]interface{}{}})
|
|
return
|
|
}
|
|
|
|
ids := make([]string, 0)
|
|
for _, id := range strings.Split(raw, ",") {
|
|
id = strings.TrimSpace(id)
|
|
if id != "" {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
if len(ids) == 0 {
|
|
c.JSON(http.StatusOK, gin.H{"data": map[string]interface{}{}})
|
|
return
|
|
}
|
|
|
|
results, err := h.stores.Users.ResolveByIDs(c.Request.Context(), ids)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "resolve failed"})
|
|
return
|
|
}
|
|
|
|
// Build map keyed by user ID for O(1) client-side lookups
|
|
m := make(map[string]interface{}, len(results))
|
|
for _, u := range results {
|
|
m[u.ID] = u
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": m})
|
|
}
|