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
+16 -6
View File
@@ -40,12 +40,15 @@ type inviteOutput struct {
Body struct {
Invite invitePayload `json:"invite"`
Guild struct {
ID string `json:"id"`
Name string `json:"name"`
IconFileID string `json:"icon_file_id,omitempty"`
Description string `json:"description,omitempty"`
MemberCount int `json:"member_count"`
IsMember bool `json:"is_member"`
ID string `json:"id"`
Name string `json:"name"`
IconFileID string `json:"icon_file_id,omitempty"`
BannerFileID string `json:"banner_file_id,omitempty"`
SplashFileID string `json:"splash_file_id,omitempty"`
AccentColor int64 `json:"accent_color,omitempty"`
Description string `json:"description,omitempty"`
MemberCount int `json:"member_count"`
IsMember bool `json:"is_member"`
} `json:"guild"`
}
}
@@ -377,6 +380,13 @@ func (s *Server) inviteOutput(ctx context.Context, user *store.User, invite *sto
if guild.IconFileID != nil {
output.Body.Guild.IconFileID = formatSnowflake(*guild.IconFileID)
}
if guild.BannerFileID != nil {
output.Body.Guild.BannerFileID = formatSnowflake(*guild.BannerFileID)
}
if guild.SplashFileID != nil {
output.Body.Guild.SplashFileID = formatSnowflake(*guild.SplashFileID)
}
output.Body.Guild.AccentColor = guild.AccentColor
if count, err := s.store.CountGuildMembers(ctx, guild.ID); err == nil {
output.Body.Guild.MemberCount = count
}