package server import ( "context" "glchat/internal/permissions" "glchat/internal/store" ) // Шаблоны серверов (AGENT.md 7.3): при создании можно выбрать встроенный // шаблон — структура комнат и трёх ролей без сообщений. Свои шаблоны — после // MVP, поэтому каталог статический. // guildTemplateRole — роль, которую создаёт шаблон. type guildTemplateRole struct { Name string Permissions []permissions.Permission Position int // Hoist — показывать участников с этой ролью отдельной группой. Hoist bool } // guildTemplateChannel — комната шаблона; `Category` задаёт категорию, внутри // которой она создаётся (пусто — верхний уровень). type guildTemplateChannel struct { Name string Type store.ChannelType Category string } // guildTemplate — встроенный шаблон сервера. type guildTemplate struct { ID string Name string Description string Roles []guildTemplateRole Channels []guildTemplateChannel } // guildTemplates — каталог шаблонов; первый — пустой сервер (поведение по // умолчанию: одна общая текстовая комната). var guildTemplates = []guildTemplate{ { ID: "empty", Name: "Пустой", Description: "Одна общая текстовая комната и роли по умолчанию.", Channels: []guildTemplateChannel{ {Name: "общий", Type: store.ChannelText}, }, }, { ID: "community", Name: "Сообщество", Description: "Информация и общение: правила, объявления, флудилка и голосовая комната.", Roles: []guildTemplateRole{ { Name: "Модератор", Permissions: []permissions.Permission{ permissions.KickMembers, permissions.TimeoutMembers, permissions.ManageMessages, permissions.ViewAuditLog, permissions.MuteMembers, permissions.DeafenMembers, permissions.MoveMembers, }, Position: 50, Hoist: true, }, }, Channels: []guildTemplateChannel{ {Name: "Информация", Type: store.ChannelCategory}, {Name: "правила", Type: store.ChannelText, Category: "Информация"}, {Name: "объявления", Type: store.ChannelText, Category: "Информация"}, {Name: "Общение", Type: store.ChannelCategory}, {Name: "общий", Type: store.ChannelText, Category: "Общение"}, {Name: "флудилка", Type: store.ChannelText, Category: "Общение"}, {Name: "Голосовой", Type: store.ChannelVoice, Category: "Общение"}, }, }, { ID: "gaming", Name: "Игровой", Description: "Текстовые комнаты для сбора и две голосовые под команды.", Roles: []guildTemplateRole{ { Name: "Игрок", Permissions: []permissions.Permission{ permissions.ConnectVoice, permissions.Speak, permissions.UseSoundboard, permissions.AttachFiles, permissions.AddReactions, permissions.EmbedLinks, }, Position: 50, Hoist: true, }, }, Channels: []guildTemplateChannel{ {Name: "Текстовые", Type: store.ChannelCategory}, {Name: "общий", Type: store.ChannelText, Category: "Текстовые"}, {Name: "поиск-игр", Type: store.ChannelText, Category: "Текстовые"}, {Name: "Голосовые", Type: store.ChannelCategory}, {Name: "Игровая 1", Type: store.ChannelVoice, Category: "Голосовые"}, {Name: "Игровая 2", Type: store.ChannelVoice, Category: "Голосовые"}, }, }, } // permissionSet собирает маску прав из списка. func permissionSet(list []permissions.Permission) permissions.Permission { var result permissions.Permission for _, permission := range list { result |= permission } return result } // findGuildTemplate ищет шаблон по идентификатору; пустой id — «Пустой». func findGuildTemplate(id string) (guildTemplate, bool) { if id == "" { id = guildTemplates[0].ID } for _, template := range guildTemplates { if template.ID == id { return template, true } } return guildTemplate{}, false } // applyGuildTemplate создаёт роли и комнаты шаблона. Администратор и // «Пользователь» уже созданы SeedGuildDefaults, поэтому шаблон добавляет // только свои роли (AGENT.md 6.3, 7.3). func (s *Server) applyGuildTemplate(ctx context.Context, guild *store.Guild, template guildTemplate) error { existing, err := s.store.ListGuildRoles(ctx, guild.ID) if err != nil { return humaError(err) } known := make(map[string]bool, len(existing)) for _, role := range existing { known[role.Name] = true } for _, role := range template.Roles { if known[role.Name] { continue } if _, err := s.store.CreateRole(ctx, store.CreateRoleParams{ GuildID: guild.ID, Name: role.Name, Permissions: uint64(permissionSet(role.Permissions)), Position: role.Position, Hoist: role.Hoist, Mentionable: true, }); err != nil { return humaError(err) } } // Категории создаются до комнат: комнаты ссылаются на них через 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 } params := store.CreateChannelParams{ GuildID: &guild.ID, Type: channel.Type, Name: channel.Name, Position: position, } if parentID != 0 && channel.Type != store.ChannelCategory { params.ParentID = &parentID } if _, err := s.store.CreateChannel(ctx, params); err != nil { return humaError(err) } position++ } return nil } // guildTemplatePayload — шаблон для клиента: предпросмотр структуры. type guildTemplatePayload struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Roles []string `json:"roles"` TextCount int `json:"text_channels"` VoiceCount int `json:"voice_channels"` Categories []string `json:"categories"` } type guildTemplateListOutput struct { Body struct { Templates []guildTemplatePayload `json:"templates"` } } // guildTemplateFromCatalog собирает описание шаблона для клиента. func guildTemplateFromCatalog(template guildTemplate) guildTemplatePayload { payload := guildTemplatePayload{ ID: template.ID, Name: template.Name, Description: template.Description, Roles: []string{"Администратор", "Пользователь"}, Categories: []string{}, } for _, role := range template.Roles { payload.Roles = append(payload.Roles, role.Name) } categories := map[string]bool{} for _, channel := range template.Channels { switch channel.Type { case store.ChannelText: payload.TextCount++ case store.ChannelVoice: payload.VoiceCount++ case store.ChannelCategory: if !categories[channel.Name] { categories[channel.Name] = true payload.Categories = append(payload.Categories, channel.Name) } case store.ChannelDM: // Личных бесед в шаблонах нет. } } return payload }