feat(social): друзья, личные беседы, присутствие, аватары и часовой пояс
По запросу пользователя (вне очереди AGENT.md §13): - главного сервера как точки входа больше нет: новичок начинает с пустым списком серверов, вход — только по приглашению или созданием своего; - друзья: поиск по логину с экранированием LIKE, заявки (POST /users/@me/relationships), принятие, удаление/отклонение, списки friends/ incoming/outgoing/blocked; - личные беседы: POST /users/@me/channels (идемпотентно), GET /users/@me/channels со собеседником, статусом и последним сообщением; сообщения в DM работают через общие ручки комнат, доступ — только участникам (посторонний получает 404, события в Gateway тоже фильтруются); - присутствие: last_seen_at обновляется при активности, «невидимка» и простой дольше двух минут выглядят как офлайн, смена статуса рассылает PRESENCE_UPDATE друзьям; - READY отдаёт dm_channels (собеседник, аватар, статус, последнее сообщение); - профиль: timezone (по умолчанию Europe/Moscow) в PATCH /users/@me, загрузка аватара POST /users/@me/avatar (проверка, что это изображение, в том числе по содержимому) и удаление DELETE /users/@me/avatar; старый файл удаляется с диска; - тесты: заявки в друзья, личные беседы и их изоляция, «невидимка», часовой пояс и аватар, PRESENCE_UPDATE другу, отсутствие событий DM у постороннего.
This commit is contained in:
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
@@ -39,11 +41,39 @@ func (s *Server) sessionContext(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
s.touchPresence(user.ID)
|
||||
ctx := context.WithValue(r.Context(), sessionContextKey{}, &sessionContextValue{User: user, Session: session})
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// touchPresence обновляет время последней активности пользователя, но не чаще
|
||||
// раза в минуту: значение нужно списку друзей (статус и «был в сети»).
|
||||
func (s *Server) touchPresence(userID uint64) {
|
||||
s.presenceMu.Lock()
|
||||
last, ok := s.presence[userID]
|
||||
now := time.Now()
|
||||
if ok && now.Sub(last) < time.Minute {
|
||||
s.presenceMu.Unlock()
|
||||
return
|
||||
}
|
||||
if len(s.presence) > 8192 {
|
||||
for id, at := range s.presence {
|
||||
if now.Sub(at) > time.Hour {
|
||||
delete(s.presence, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.presence[userID] = now
|
||||
s.presenceMu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := s.store.TouchLastSeen(ctx, userID); err != nil {
|
||||
s.logger.DebugContext(ctx, "failed to update last seen", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
// sessionToken читает токен сессии из cookie (браузер) или Bearer (desktop).
|
||||
func sessionToken(r *http.Request) string {
|
||||
if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" {
|
||||
|
||||
Reference in New Issue
Block a user