From afcdf9c1c4980dab8889655f08371bdac17ea85f Mon Sep 17 00:00:00 2001 From: grendervill Date: Mon, 21 Sep 2026 18:16:18 +0300 Subject: [PATCH] =?UTF-8?q?fix(guilds):=20=D1=84=D0=BE=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BD=D0=B0=D1=82=D1=8B=20=D0=B2=20READY=20=D0=B8?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=B2?= =?UTF-8?q?=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReadyChannel отдаёт background_file_id: после перезагрузки фон комнаты больше не теряется до REST-запроса (AGENT.md 7.5) - шаблон больше не создаёт категории дважды: они перечислены в шаблоне и раньше создавались ещё и по ссылке из комнаты - тест: структура «Сообщества» ровно 7 комнат без повторов --- internal/gateway/gateway.go | 3 ++ internal/gateway/ready.go | 3 ++ internal/server/guild_templates.go | 41 ++++++++++++++----------- internal/server/guild_templates_test.go | 33 ++++++++++++++++++++ 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index bae666d..7515dce 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -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"` diff --git a/internal/gateway/ready.go b/internal/gateway/ready.go index 0cb882d..57503c4 100644 --- a/internal/gateway/ready.go +++ b/internal/gateway/ready.go @@ -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) } diff --git a/internal/server/guild_templates.go b/internal/server/guild_templates.go index ba74d99..36d30ae 100644 --- a/internal/server/guild_templates.go +++ b/internal/server/guild_templates.go @@ -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 { diff --git a/internal/server/guild_templates_test.go b/internal/server/guild_templates_test.go index 901805d..2085909 100644 --- a/internal/server/guild_templates_test.go +++ b/internal/server/guild_templates_test.go @@ -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)) + } +}