Files
glchat/internal/server/guild_templates.go
T

238 lines
8.0 KiB
Go
Raw Normal View History

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
}