This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/presence.go
Jeffrey Smith a8204859b5
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 3s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
Feat v0.6.15 user display audit
Add batch user resolve endpoint, sw.users SDK module, and migrate all
surfaces to show display_name instead of UUIDs. Canonical fallback chain:
display_name → username → "Unknown".

- GET /api/v1/users/resolve?ids=... (max 100, map response)
- sw.users.resolve(), resolveMany(), displayName() with 60s cache
- Chat participants resolved from users table, not snapshot
- Admin users/teams/groups/team-admin show display_name
- Chat-core participants.display_name column deprecated
- 5 new handler tests, OpenAPI spec updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:19:03 +00:00

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})
}