feat(invites): страница приглашения со splash и переопределением
- миграция 00015: переопределение splash у приглашения (AGENT.md 7.9)
- GET /invites/{code} доступен без сессии: гость по ссылке видит карточку
сервера, описание, число участников и пригласившего
- POST/DELETE /invites/{code}/background: своя картинка страницы под правом
создателя или MANAGE_GUILD, файл удаляется при замене и снятии
- клиент: маршрут /invite/:code с фоном (переопределение → splash → баннер),
карточкой сервера, кнопками «Присоединиться»/«Открыть сервер» и входом с
возвратом на страницу (?next=)
- тесты: Go (карточка, загрузка, права, снятие) и Vitest (splash, гость,
присоединение, недействительная ссылка)
This commit is contained in:
@@ -17,6 +17,9 @@ type Invite struct {
|
||||
MaxAgeSec int
|
||||
ExpiresAt *time.Time
|
||||
CreatedAt time.Time
|
||||
// BackgroundFileID — переопределение splash для этого приглашения
|
||||
// (AGENT.md 7.9): страница приглашения показывает свою картинку.
|
||||
BackgroundFileID *uint64
|
||||
}
|
||||
|
||||
// CreateInviteParams — параметры приглашения.
|
||||
@@ -30,7 +33,7 @@ type CreateInviteParams struct {
|
||||
}
|
||||
|
||||
const inviteColumns = `code, guild_id, channel_id, creator_id, max_uses, uses,
|
||||
max_age_sec, expires_at, created_at`
|
||||
max_age_sec, expires_at, created_at, background_file_id`
|
||||
|
||||
// CreateInvite сохраняет приглашение с кодом, лимитом использований и сроком.
|
||||
func (s *Store) CreateInvite(ctx context.Context, params CreateInviteParams) (*Invite, error) {
|
||||
@@ -54,6 +57,19 @@ func (s *Store) GetInvite(ctx context.Context, code string) (*Invite, error) {
|
||||
return scanInvite(row)
|
||||
}
|
||||
|
||||
// SetInviteBackground задаёт или снимает переопределение splash приглашения.
|
||||
func (s *Store) SetInviteBackground(ctx context.Context, code string, fileID *uint64) (*Invite, error) {
|
||||
result, err := s.writer.ExecContext(ctx,
|
||||
`UPDATE invites SET background_file_id = ? WHERE code = ?`, nullableID(fileID), code)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err == nil && affected == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return s.GetInvite(ctx, code)
|
||||
}
|
||||
|
||||
// ListGuildInvites возвращает приглашения сервера (для панели управления).
|
||||
func (s *Store) ListGuildInvites(ctx context.Context, guildID uint64, limit int) ([]Invite, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
@@ -132,19 +148,21 @@ func (s *Store) ListPublicGuilds(ctx context.Context, limit int) ([]Guild, error
|
||||
|
||||
func scanInvite(scanner interface{ Scan(...any) error }) (*Invite, error) {
|
||||
var (
|
||||
invite Invite
|
||||
channelID sql.NullInt64
|
||||
creatorID sql.NullInt64
|
||||
expiresAt sql.NullString
|
||||
createdAt string
|
||||
invite Invite
|
||||
channelID sql.NullInt64
|
||||
creatorID sql.NullInt64
|
||||
expiresAt sql.NullString
|
||||
createdAt string
|
||||
background sql.NullInt64
|
||||
)
|
||||
err := scanner.Scan(&invite.Code, &invite.GuildID, &channelID, &creatorID, &invite.MaxUses,
|
||||
&invite.Uses, &invite.MaxAgeSec, &expiresAt, &createdAt)
|
||||
&invite.Uses, &invite.MaxAgeSec, &expiresAt, &createdAt, &background)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
invite.ChannelID = optionalID(channelID)
|
||||
invite.CreatorID = optionalID(creatorID)
|
||||
invite.BackgroundFileID = optionalID(background)
|
||||
if expiresAt.Valid {
|
||||
value := parseTimestamp(expiresAt.String)
|
||||
invite.ExpiresAt = &value
|
||||
|
||||
Reference in New Issue
Block a user