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:
+22
-2
@@ -31,6 +31,10 @@ type User struct {
|
||||
DeletedAt *time.Time
|
||||
// OnboardingCompletedAt заполняется после первичной настройки (AGENT.md 7.2).
|
||||
OnboardingCompletedAt *time.Time
|
||||
// LastSeenAt — время последней активности (для статусов друзей).
|
||||
LastSeenAt *time.Time
|
||||
// Timezone — часовой пояс пользователя (IANA, по умолчанию Europe/Moscow).
|
||||
Timezone string
|
||||
}
|
||||
|
||||
// CreateUserParams — данные новой учётной записи: шифрование и blind index
|
||||
@@ -48,7 +52,7 @@ type CreateUserParams struct {
|
||||
const userColumns = `id, username, display_name, email_enc, password_hash, avatar_file_id,
|
||||
banner_file_id, bio, status, custom_status, custom_status_emoji, flags,
|
||||
is_instance_admin, badges_json, locale, created_at, updated_at, deleted_at,
|
||||
onboarding_completed_at`
|
||||
onboarding_completed_at, last_seen_at, timezone`
|
||||
|
||||
func (s *Store) CreateUser(ctx context.Context, params CreateUserParams) (*User, error) {
|
||||
if params.ID == 0 {
|
||||
@@ -235,6 +239,14 @@ func (s *Store) MarkOnboardingCompleted(ctx context.Context, id uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearAvatar убирает ссылку на файл аватара.
|
||||
func (s *Store) ClearAvatar(ctx context.Context, id uint64) error {
|
||||
_, err := s.writer.ExecContext(ctx,
|
||||
`UPDATE users SET avatar_file_id = NULL, updated_at = ? WHERE id = ? AND deleted_at IS NULL`,
|
||||
s.Now(), int64(id))
|
||||
return err
|
||||
}
|
||||
|
||||
// ListUsers отдаёт страницу пользователей для админ-панели инстанса (AGENT.md 6.5).
|
||||
func (s *Store) ListUsers(ctx context.Context, limit, offset int) ([]User, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
@@ -273,13 +285,14 @@ func scanUser(scanner interface{ Scan(...any) error }) (*User, error) {
|
||||
updatedAt string
|
||||
deletedAt sql.NullString
|
||||
onboardingAt sql.NullString
|
||||
lastSeenAt sql.NullString
|
||||
emailEncrypted string
|
||||
)
|
||||
err := scanner.Scan(
|
||||
&user.ID, &user.Username, &user.DisplayName, &emailEncrypted, &user.PasswordHash,
|
||||
&avatarID, &bannerID, &user.Bio, &user.Status, &user.CustomStatus, &user.CustomStatusEmoji,
|
||||
&user.Flags, &isAdmin, &badges, &user.Locale, &createdAt, &updatedAt, &deletedAt,
|
||||
&onboardingAt,
|
||||
&onboardingAt, &lastSeenAt, &user.Timezone,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
@@ -306,6 +319,13 @@ func scanUser(scanner interface{ Scan(...any) error }) (*User, error) {
|
||||
value := parseTimestamp(onboardingAt.String)
|
||||
user.OnboardingCompletedAt = &value
|
||||
}
|
||||
if lastSeenAt.Valid {
|
||||
value := parseTimestamp(lastSeenAt.String)
|
||||
user.LastSeenAt = &value
|
||||
}
|
||||
if user.Timezone == "" {
|
||||
user.Timezone = "Europe/Moscow"
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user