Changeset 0.29.0 (#195)
This commit is contained in:
@@ -5,6 +5,8 @@ package handlers
|
||||
// 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.
|
||||
//
|
||||
// v0.29.0: Raw SQL replaced with PresenceStore + UserStore methods.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -13,21 +15,23 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/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 PresenceHeartbeat(c *gin.Context) {
|
||||
func (h *PresenceHandler) Heartbeat(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
_, err := database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
INSERT INTO user_presence (user_id, last_seen, status)
|
||||
VALUES ($1, NOW(), 'online')
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET last_seen = NOW(), status = 'online'
|
||||
`), userID)
|
||||
if err != nil {
|
||||
if err := h.stores.Presence.Heartbeat(c.Request.Context(), userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "presence update failed"})
|
||||
return
|
||||
}
|
||||
@@ -36,7 +40,7 @@ func PresenceHeartbeat(c *gin.Context) {
|
||||
|
||||
// PresenceQuery returns online/offline status for a list of user IDs.
|
||||
// Query param: ?users=uuid1,uuid2,...
|
||||
func PresenceQuery(c *gin.Context) {
|
||||
func (h *PresenceHandler) Query(c *gin.Context) {
|
||||
raw := c.Query("users")
|
||||
if raw == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"presence": map[string]string{}})
|
||||
@@ -47,71 +51,34 @@ func PresenceQuery(c *gin.Context) {
|
||||
ids = ids[:100]
|
||||
}
|
||||
|
||||
threshold := time.Now().Add(-presenceOnlineThreshold)
|
||||
|
||||
result := make(map[string]string, len(ids))
|
||||
// Trim whitespace
|
||||
cleaned := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
var lastSeen time.Time
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT last_seen FROM user_presence WHERE user_id = $1
|
||||
`), id).Scan(&lastSeen)
|
||||
if err != nil || lastSeen.Before(threshold) {
|
||||
result[id] = "offline"
|
||||
} else {
|
||||
result[id] = "online"
|
||||
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 — matches against username and display_name.
|
||||
// Returns at most 20 results. Excludes the calling user.
|
||||
func SearchUsers(c *gin.Context) {
|
||||
// GET /api/v1/users/search?q=alice
|
||||
func (h *PresenceHandler) SearchUsers(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
|
||||
query := database.Q(`
|
||||
SELECT id, username, COALESCE(display_name, '') AS display_name, COALESCE(handle, '') AS handle
|
||||
FROM users
|
||||
WHERE is_active = true AND id != $1
|
||||
`)
|
||||
args := []interface{}{userID}
|
||||
|
||||
if q != "" {
|
||||
query += database.Q(` AND (LOWER(username) LIKE $2 OR LOWER(display_name) LIKE $3 OR LOWER(handle) LIKE $4)`)
|
||||
pattern := "%" + strings.ToLower(q) + "%"
|
||||
args = append(args, pattern, pattern, pattern)
|
||||
}
|
||||
|
||||
query += ` ORDER BY username LIMIT 20`
|
||||
|
||||
rows, err := database.DB.QueryContext(c.Request.Context(), query, args...)
|
||||
results, err := h.stores.Users.SearchActive(c.Request.Context(), userID, q)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "search failed"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type userResult struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Handle string `json:"handle"`
|
||||
}
|
||||
|
||||
results := []userResult{}
|
||||
for rows.Next() {
|
||||
var u userResult
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Handle); err != nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, u)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"users": results})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user