feat(guilds): оформление сервера — иконка, баннер, splash и акцентный цвет

Фаза 4 (AGENT.md 7.4): сервер можно оформить, поля в схеме были с Фазы 1, но
ручек и отдачи в API не было.

- `POST/DELETE /guilds/{id}/appearance/{icon|banner|splash}` (multipart,
  MANAGE_GUILD, только изображения до 8 МБ): файл сохраняется назначением
  `guild_icon|guild_banner|guild_splash`, предыдущий удаляется, пишется аудит
  и уходит `GUILD_UPDATE`; ответ — обновлённый сервер;
- `PATCH /guilds/{id}` принимает `accent_color` (0 — цвет темы);
- оформление отдаётся в READY, в списке серверов, в детальной карточке и в
  предпросмотре приглашения — клиенту не нужен отдельный запрос;
- анимированные GIF/APNG/WebP сохраняются как есть: изображения не
  перекодируются, поэтому анимация не теряется;
- тесты: загрузка и очистка всех трёх видов, замена файла, запрет без
  MANAGE_GUILD и без сессии, отказ для не-изображения, акцентный цвет.
This commit is contained in:
2026-09-20 23:26:46 +03:00
parent f1a195ec9d
commit 02a5b99418
9 changed files with 492 additions and 30 deletions
+24 -12
View File
@@ -128,18 +128,22 @@ func (s *Snapshot) Build(ctx context.Context, user *store.User) (*Ready, error)
func (s *Snapshot) buildGuild(ctx context.Context, guild store.Guild, user *store.User, resolved permissions.Resolved) (ReadyGuild, error) {
readyGuild := ReadyGuild{
ID: formatID(guild.ID),
Name: guild.Name,
OwnerID: formatID(guild.OwnerID),
IsMain: guild.IsMain,
Channels: []ReadyChannel{},
Roles: []ReadyRole{},
MemberIDs: []string{},
MyRoles: []string{},
MyPerms: []string{},
Emojis: []ReadyEmoji{},
VoiceStates: []ReadyVoiceState{},
Sounds: []ReadySound{},
ID: formatID(guild.ID),
Name: guild.Name,
OwnerID: formatID(guild.OwnerID),
IsMain: guild.IsMain,
AccentColor: guild.AccentColor,
IconFileID: optionalFormatID(guild.IconFileID),
BannerFileID: optionalFormatID(guild.BannerFileID),
SplashFileID: optionalFormatID(guild.SplashFileID),
Channels: []ReadyChannel{},
Roles: []ReadyRole{},
MemberIDs: []string{},
MyRoles: []string{},
MyPerms: []string{},
Emojis: []ReadyEmoji{},
VoiceStates: []ReadyVoiceState{},
Sounds: []ReadySound{},
}
// Кастомные эмодзи сервера: клиент показывает их в пикере и в тексте.
@@ -362,6 +366,14 @@ func visibleStatus(status string, lastSeen *time.Time) string {
return status
}
// optionalFormatID отдаёт идентификатор файла или пустую строку.
func optionalFormatID(id *uint64) string {
if id == nil || *id == 0 {
return ""
}
return formatID(*id)
}
func formatID(id uint64) string {
if id == 0 {
return ""