fix(guilds): фон комнаты в READY и без дублей категорий в шаблонах

- ReadyChannel отдаёт background_file_id: после перезагрузки фон комнаты больше
  не теряется до REST-запроса (AGENT.md 7.5)
- шаблон больше не создаёт категории дважды: они перечислены в шаблоне и
  раньше создавались ещё и по ссылке из комнаты
- тест: структура «Сообщества» ровно 7 комнат без повторов
This commit is contained in:
2026-09-21 18:16:18 +03:00
parent b43452aa04
commit afcdf9c1c4
4 changed files with 62 additions and 18 deletions
+3
View File
@@ -132,6 +132,9 @@ type ReadyChannel struct {
Slowmode int `json:"slowmode_seconds,omitempty"`
CanSend bool `json:"can_send"`
CanConnect bool `json:"can_connect"`
// BackgroundFileID — фон комнаты: приходит и в READY, и в CHANNEL_UPDATE
// (AGENT.md 7.5), иначе после перезагрузки фон терялся бы до REST-запроса.
BackgroundFileID string `json:"background_file_id,omitempty"`
// Поля личных бесед (type = dm): собеседник и последнее сообщение.
RecipientID string `json:"recipient_id,omitempty"`
IconFileID string `json:"icon_file_id,omitempty"`
+3
View File
@@ -271,6 +271,9 @@ func channelPayload(channel store.Channel, canSend, canConnect bool) ReadyChanne
if channel.GuildID != nil {
payload.GuildID = formatID(*channel.GuildID)
}
if channel.BackgroundFileID != nil {
payload.BackgroundFileID = formatID(*channel.BackgroundFileID)
}
if channel.ParentID != nil {
payload.ParentID = formatID(*channel.ParentID)
}
+23 -18
View File
@@ -150,26 +150,31 @@ func (s *Server) applyGuildTemplate(ctx context.Context, guild *store.Guild, tem
}
}
// Категории создаются до комнат: комнаты ссылаются на них через parent_id.
// Категории создаём первыми: комнаты ссылаются на них через parent_id.
// В шаблоне категория перечислена отдельной записью, поэтому повторно её
// не создаём — только запоминаем идентификатор.
categories := map[string]uint64{}
position := 0
for _, channel := range template.Channels {
parentID := uint64(0)
if channel.Category != "" {
id, ok := categories[channel.Category]
if !ok {
created, err := s.store.CreateChannel(ctx, store.CreateChannelParams{
GuildID: &guild.ID, Type: store.ChannelCategory,
Name: channel.Category, Position: position,
})
if err != nil {
return humaError(err)
}
categories[channel.Category] = created.ID
position++
id = created.ID
}
parentID = id
if channel.Type != store.ChannelCategory {
continue
}
if _, exists := categories[channel.Name]; exists {
continue
}
created, err := s.store.CreateChannel(ctx, store.CreateChannelParams{
GuildID: &guild.ID, Type: store.ChannelCategory,
Name: channel.Name, Position: position,
})
if err != nil {
return humaError(err)
}
categories[channel.Name] = created.ID
position++
}
for _, channel := range template.Channels {
if channel.Type == store.ChannelCategory {
continue
}
params := store.CreateChannelParams{
GuildID: &guild.ID,
@@ -177,7 +182,7 @@ func (s *Server) applyGuildTemplate(ctx context.Context, guild *store.Guild, tem
Name: channel.Name,
Position: position,
}
if parentID != 0 && channel.Type != store.ChannelCategory {
if parentID, ok := categories[channel.Category]; ok {
params.ParentID = &parentID
}
if _, err := s.store.CreateChannel(ctx, params); err != nil {
+33
View File
@@ -157,3 +157,36 @@ func contains(list []string, value string) bool {
}
return false
}
func TestGuildFromTemplateCreatesNoDuplicates(t *testing.T) {
srv, _ := newTestServer(t)
owner := registerAndLogin(t, srv, "tpl_dup", "tpl-dup@example.com")
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds",
`{"name":"Дубли","template_id":"community"}`, owner)
guildID := decodeResponse[struct {
Guild struct {
ID string `json:"id"`
} `json:"guild"`
}](t, created).Guild.ID
channels := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/channels", "", owner)
list := decodeResponse[struct {
Channels []struct {
Name string `json:"name"`
Type string `json:"type"`
} `json:"channels"`
}](t, channels)
seen := map[string]int{}
for _, channel := range list.Channels {
seen[channel.Type+":"+channel.Name]++
}
for key, count := range seen {
if count > 1 {
t.Fatalf("комната %s создана %d раза", key, count)
}
}
// «Сообщество»: 2 категории, 4 текстовые и 1 голосовая.
if len(list.Channels) != 7 {
t.Fatalf("комнат %d, ожидалось 7", len(list.Channels))
}
}