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:
@@ -199,9 +199,8 @@ func (s *Service) Register(ctx context.Context, in RegisterInput) (*store.User,
|
||||
return nil, "", nil, fmt.Errorf("create user: %w", err)
|
||||
}
|
||||
|
||||
if err := s.joinMainGuild(ctx, user.ID); err != nil {
|
||||
s.logger.WarnContext(ctx, "failed to join main guild", slog.Any("error", err))
|
||||
}
|
||||
// Новый пользователь попадает в пустой список серверов: вход только по
|
||||
// приглашению или созданием своего сервера (решение пользователя, 2026-09-20).
|
||||
|
||||
token, session, err := s.createSession(ctx, user.ID, in.UserAgent, in.IP)
|
||||
if err != nil {
|
||||
@@ -475,27 +474,6 @@ func (s *Service) createSession(ctx context.Context, userID uint64, userAgent, i
|
||||
return token, session, nil
|
||||
}
|
||||
|
||||
// joinMainGuild добавляет нового пользователя на главный сервер с ролью
|
||||
// «Пользователь» (AGENT.md 7.3).
|
||||
func (s *Service) joinMainGuild(ctx context.Context, userID uint64) error {
|
||||
settings, err := s.settings.InstanceSettings(ctx)
|
||||
if err != nil || settings.MainGuildID == 0 {
|
||||
return err
|
||||
}
|
||||
if _, err := s.store.AddGuildMember(ctx, settings.MainGuildID, userID, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
defaultRole, err := s.store.DefaultRole(ctx, settings.MainGuildID)
|
||||
if err != nil {
|
||||
// Роль по умолчанию может отсутствовать (сервер без ролей) — это не мешает
|
||||
// автовступлению, поэтому ошибку не возвращаем, но фиксируем в логе.
|
||||
s.logger.WarnContext(ctx, "main guild has no default role",
|
||||
slog.Uint64("guild_id", settings.MainGuildID), slog.Any("error", err))
|
||||
return nil
|
||||
}
|
||||
return s.store.AssignRole(ctx, settings.MainGuildID, userID, defaultRole.ID)
|
||||
}
|
||||
|
||||
// checkLockout защищает от перебора: серия неудач блокирует попытки на окно.
|
||||
func (s *Service) checkLockout(ctx context.Context, userID uint64) error {
|
||||
events, err := s.store.ListSecurityEvents(ctx, userID, 20)
|
||||
|
||||
Reference in New Issue
Block a user