2026-09-21 18:04:52 +03:00
package server
import (
"context"
2026-09-26 16:45:10 +03:00
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"github.com/danielgtaylor/huma/v2"
2026-09-21 18:04:52 +03:00
"glchat/internal/permissions"
"glchat/internal/store"
)
// Шаблоны серверов (AGENT.md 7.3): при создании можно выбрать встроенный
2026-09-26 16:45:10 +03:00
// шаблон — структура комнат и трёх ролей без сообщений — или свой, снятый с
// существующего сервера. Встроенный каталог статический (guildTemplates),
// личные шаблоны живут в БД (guild_templates) и приватны для владельца.
2026-09-21 18:04:52 +03:00
// guildTemplateRole — роль, которую создаёт шаблон.
type guildTemplateRole struct {
Name string
Permissions [ ] permissions . Permission
2026-09-26 16:45:10 +03:00
// Color — цвет роли в RGB (0 — по умолчанию).
Color int64
Position int
2026-09-21 18:04:52 +03:00
// Hoist — показывать участников с этой ролью отдельной группой.
Hoist bool
2026-09-26 16:45:10 +03:00
// Mentionable — роль разрешено упоминать.
Mentionable bool
2026-09-21 18:04:52 +03:00
}
// guildTemplateChannel — комната шаблона; `Category` задаёт категорию, внутри
// которой она создаётся (пусто — верхний уровень).
type guildTemplateChannel struct {
2026-09-26 16:45:10 +03:00
Name string
Type store . ChannelType
Category string
SlowmodeSeconds int
UserLimit int
2026-09-21 18:04:52 +03:00
}
2026-09-26 16:45:10 +03:00
// guildTemplate — шаблон сервера: встроенный из каталога или личный,
// разобранный из structure_json (AGENT.md 7.3).
2026-09-21 18:04:52 +03:00
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 ,
} ,
2026-09-26 16:45:10 +03:00
Position : 50 ,
Hoist : true ,
Mentionable : true ,
2026-09-21 18:04:52 +03:00
} ,
} ,
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 ,
} ,
2026-09-26 16:45:10 +03:00
Position : 50 ,
Hoist : true ,
Mentionable : true ,
2026-09-21 18:04:52 +03:00
} ,
} ,
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 ,
2026-09-26 16:45:10 +03:00
Color : role . Color ,
2026-09-21 18:04:52 +03:00
Permissions : uint64 ( permissionSet ( role . Permissions ) ) ,
Position : role . Position ,
Hoist : role . Hoist ,
2026-09-26 16:45:10 +03:00
Mentionable : role . Mentionable ,
2026-09-21 18:04:52 +03:00
} ) ; err != nil {
return humaError ( err )
}
}
2026-09-21 18:16:18 +03:00
// Категории создаём первыми: комнаты ссылаются на них через parent_id.
// В шаблоне категория перечислена отдельной записью, поэтому повторно её
// не создаём — только запоминаем идентификатор.
2026-09-21 18:04:52 +03:00
categories := map [ string ] uint64 { }
position := 0
for _ , channel := range template . Channels {
2026-09-21 18:16:18 +03:00
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
2026-09-21 18:04:52 +03:00
}
params := store . CreateChannelParams {
2026-09-26 16:45:10 +03:00
GuildID : & guild . ID ,
Type : channel . Type ,
Name : channel . Name ,
Position : position ,
SlowmodeSeconds : channel . SlowmodeSeconds ,
UserLimit : channel . UserLimit ,
2026-09-21 18:04:52 +03:00
}
2026-09-21 18:16:18 +03:00
if parentID , ok := categories [ channel . Category ] ; ok {
2026-09-21 18:04:52 +03:00
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" `
2026-09-26 16:45:10 +03:00
// CreatedAt и SourceGuildID заполнены только у личных шаблонов.
CreatedAt string ` json:"created_at,omitempty" `
SourceGuildID string ` json:"source_guild_id,omitempty" `
2026-09-21 18:04:52 +03:00
}
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
}
2026-09-26 16:45:10 +03:00
// Личные шаблоны серверов (AGENT.md 7.3, Фаза 7): владелец сохраняет структуру
// своего сервера и создаёт по ней новые. Шаблон приватный — видит и использует
// только владелец; чужой шаблон неотличим от несуществующего (AGENT.md 9.7).
//
// Записи в аудит на CRUD личных шаблонов нет осознанно: шаблон — приватная
// сущность пользователя, у неё нет сервера, а журнал аудита живёт на сервере
// (AGENT.md 7.10). Единственное действие, попадающее в аудит, — создание
// сервера по шаблону: там причина содержит template=<id>.
const (
// personalTemplatePrefix — личный шаблон в template_id: personal:<id>.
personalTemplatePrefix = "personal:"
// maxUserGuildTemplateName и maxUserGuildTemplateDescription повторяют
// ограничения имён сервера и коротких описаний.
maxUserGuildTemplateName = 64
maxUserGuildTemplateDescription = 200
// maxUserGuildTemplateRoles и maxUserGuildTemplateChannels ограничивают
// саму структуру: категории считаются комнатами.
maxUserGuildTemplateRoles = 50
maxUserGuildTemplateChannels = 100
)
// userGuildTemplateStructure — structure_json личного шаблона. Формат повторяет
// внутренний guildTemplate: роли (кроме роли по умолчанию) с цветом, позицией
// и строкой прав, затем категории и комнаты. Права — строкой как в API ролей
// («VIEW_CHANNEL|SEND_MESSAGES», «NONE» — нет прав): маска uint64 не теряет
// точность в JSON. Числовая строка при разборе тоже принимается.
type userGuildTemplateStructure struct {
Roles [ ] userGuildTemplateRole ` json:"roles" `
Channels [ ] userGuildTemplateChannel ` json:"channels" `
}
type userGuildTemplateRole struct {
Name string ` json:"name" `
Color int64 ` json:"color" `
Permissions string ` json:"permissions" `
Position int ` json:"position" `
Hoist bool ` json:"hoist" `
Mentionable bool ` json:"mentionable" `
}
type userGuildTemplateChannel struct {
Name string ` json:"name" `
Type string ` json:"type" `
Category string ` json:"category,omitempty" `
Position int ` json:"position" `
SlowmodeSeconds int ` json:"slowmode_seconds,omitempty" `
UserLimit int ` json:"user_limit,omitempty" `
}
type userGuildTemplateOutput struct {
Body struct {
Template guildTemplatePayload ` json:"template" `
}
}
type userGuildTemplateListOutput struct {
Body struct {
Templates [ ] guildTemplatePayload ` json:"templates" `
}
}
type guildTemplateDeleteOutput struct {
Body struct {
OK bool ` json:"ok" `
}
}
// registerUserGuildTemplateRoutes описывает ручки личных шаблонов серверов.
func ( s * Server ) registerUserGuildTemplateRoutes ( api huma . API , security [ ] map [ string ] [ ] string ) {
huma . Register ( api , huma . Operation {
OperationID : "createUserGuildTemplate" ,
Method : http . MethodPost ,
Path : "/guild-templates" ,
Summary : "Сохранить структуру сервера личным шаблоном" ,
Tags : [ ] string { "Guilds" } ,
Security : security ,
} , func ( ctx context . Context , input * struct {
Body struct {
GuildID string ` json:"guild_id" `
Name string ` json:"name,omitempty" `
Description string ` json:"description,omitempty" `
}
} ,
) ( * userGuildTemplateOutput , error ) {
user , _ , err := requireUser ( ctx )
if err != nil {
return nil , err
}
guildID , err := parseID ( "guild_id" , input . Body . GuildID )
if err != nil {
return nil , err
}
// Право MANAGE_GUILD: администратор инстанса проходит всегда
// (permissions.Resolved.IsInstanceAdmin).
resolved , err := s . guildPermissions ( ctx , guildID , user )
if err != nil {
return nil , err
}
if ! resolved . Has ( permissions . ManageGuild ) {
return nil , humaErrorStatus ( http . StatusForbidden , "perm.denied" , "permission denied" )
}
guild , err := s . store . GetGuild ( ctx , guildID )
if err != nil {
return nil , humaError ( err )
}
name := strings . TrimSpace ( input . Body . Name )
if name == "" {
name = guild . Name
}
if len ( [ ] rune ( name ) ) > maxUserGuildTemplateName {
return nil , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "template name must be 1-64 characters" )
}
description := strings . TrimSpace ( input . Body . Description )
if len ( [ ] rune ( description ) ) > maxUserGuildTemplateDescription {
return nil , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "template description must be at most 200 characters" )
}
count , err := s . store . CountUserGuildTemplates ( ctx , user . ID )
if err != nil {
return nil , humaError ( err )
}
if count >= store . MaxUserGuildTemplates {
return nil , humaErrorStatus ( http . StatusConflict , "template.limit_reached" , "personal guild template limit is reached" )
}
structure , err := s . buildUserGuildTemplateStructure ( ctx , guildID )
if err != nil {
return nil , err
}
if len ( structure . Roles ) > maxUserGuildTemplateRoles || len ( structure . Channels ) > maxUserGuildTemplateChannels {
return nil , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "guild structure is too large for a template: at most 50 roles and 100 channels" )
}
encoded , err := json . Marshal ( structure )
if err != nil {
return nil , humaError ( err )
}
if len ( encoded ) > store . MaxUserGuildTemplateBytes {
return nil , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "guild template structure is too large" )
}
created , err := s . store . CreateUserGuildTemplate ( ctx , store . CreateUserGuildTemplateParams {
OwnerID : user . ID ,
Name : name ,
Description : description ,
Structure : encoded ,
SourceGuildID : & guildID ,
} )
if err != nil {
switch {
case errors . Is ( err , store . ErrTemplateLimitReached ) :
return nil , humaErrorStatus ( http . StatusConflict , "template.limit_reached" , "personal guild template limit is reached" )
case errors . Is ( err , store . ErrTemplateTooLarge ) :
return nil , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "guild template structure is too large" )
}
return nil , humaError ( err )
}
output := & userGuildTemplateOutput { }
output . Body . Template = s . userGuildTemplatePayload ( * created )
return output , nil
} )
huma . Register ( api , huma . Operation {
OperationID : "listUserGuildTemplates" ,
Method : http . MethodGet ,
Path : "/guild-templates/mine" ,
Summary : "Свои шаблоны серверов" ,
Tags : [ ] string { "Guilds" } ,
Security : security ,
} , func ( ctx context . Context , _ * struct { } ) ( * userGuildTemplateListOutput , error ) {
user , _ , err := requireUser ( ctx )
if err != nil {
return nil , err
}
templates , err := s . store . ListUserGuildTemplates ( ctx , user . ID )
if err != nil {
return nil , humaError ( err )
}
output := & userGuildTemplateListOutput { }
output . Body . Templates = make ( [ ] guildTemplatePayload , 0 , len ( templates ) )
for _ , template := range templates {
output . Body . Templates = append ( output . Body . Templates , s . userGuildTemplatePayload ( template ) )
}
return output , nil
} )
huma . Register ( api , huma . Operation {
OperationID : "deleteUserGuildTemplate" ,
Method : http . MethodDelete ,
Path : "/guild-templates/{template_id}" ,
Summary : "Удалить свой шаблон сервера" ,
Tags : [ ] string { "Guilds" } ,
Security : security ,
} , func ( ctx context . Context , input * struct {
TemplateID string ` path:"template_id" `
} ,
) ( * guildTemplateDeleteOutput , error ) {
user , _ , err := requireUser ( ctx )
if err != nil {
return nil , err
}
// Принимаем и «personal:<id>» (как в списке шаблонов клиента), и сам id.
templateID , err := parseID ( "template_id" , strings . TrimPrefix ( input . TemplateID , personalTemplatePrefix ) )
if err != nil {
return nil , err
}
deleted , err := s . store . DeleteUserGuildTemplate ( ctx , templateID , user . ID )
if err != nil {
return nil , humaError ( err )
}
if ! deleted {
// Чужой шаблон неотличим от несуществующего (AGENT.md 9.7).
return nil , humaErrorStatus ( http . StatusNotFound , "not_found" , "guild template not found" )
}
output := & guildTemplateDeleteOutput { }
output . Body . OK = true
return output , nil
} )
}
// buildUserGuildTemplateStructure снимает структуру сервера: роли (кроме роли
// по умолчанию — «Пользователь» создаётся для каждого сервера сам, AGENT.md 6.3)
// и все комнаты вместе с категориями.
func ( s * Server ) buildUserGuildTemplateStructure ( ctx context . Context , guildID uint64 ) ( userGuildTemplateStructure , error ) {
roles , err := s . store . ListGuildRoles ( ctx , guildID )
if err != nil {
return userGuildTemplateStructure { } , humaError ( err )
}
structure := userGuildTemplateStructure {
Roles : make ( [ ] userGuildTemplateRole , 0 , len ( roles ) ) ,
Channels : make ( [ ] userGuildTemplateChannel , 0 , 8 ) ,
}
for _ , role := range roles {
if role . IsDefault {
continue
}
structure . Roles = append ( structure . Roles , userGuildTemplateRole {
Name : role . Name ,
Color : role . Color ,
Permissions : permissions . Permission ( role . Permissions ) . String ( ) ,
Position : role . Position ,
Hoist : role . Hoist ,
Mentionable : role . Mentionable ,
} )
}
channels , err := s . store . ListGuildChannels ( ctx , guildID )
if err != nil {
return userGuildTemplateStructure { } , humaError ( err )
}
// Комната ссылается на категорию по имени: идентификаторы в новом сервере
// будут другими (AGENT.md 6.4).
categories := make ( map [ uint64 ] string , len ( channels ) )
for _ , channel := range channels {
if channel . Type == store . ChannelCategory {
categories [ channel . ID ] = channel . Name
}
}
for _ , channel := range channels {
if channel . Type == store . ChannelDM {
continue
}
item := userGuildTemplateChannel {
Name : channel . Name ,
Type : string ( channel . Type ) ,
Position : channel . Position ,
SlowmodeSeconds : channel . SlowmodeSeconds ,
UserLimit : channel . UserLimit ,
}
if channel . ParentID != nil {
item . Category = categories [ * channel . ParentID ]
}
structure . Channels = append ( structure . Channels , item )
}
return structure , nil
}
// structureToGuildTemplate превращает сохранённую структуру во внутренний
// шаблон: порядок комнат задаёт position. Битый шаблон — ошибка целиком:
// половина сервера хуже, чем отказ.
func structureToGuildTemplate ( structure userGuildTemplateStructure ) ( guildTemplate , error ) {
template := guildTemplate {
Roles : make ( [ ] guildTemplateRole , 0 , len ( structure . Roles ) ) ,
Channels : make ( [ ] guildTemplateChannel , 0 , len ( structure . Channels ) ) ,
}
for _ , role := range structure . Roles {
name := strings . TrimSpace ( role . Name )
if name == "" {
return guildTemplate { } , errors . New ( "role name must not be empty" )
}
mask , err := parseTemplatePermissions ( role . Permissions )
if err != nil {
return guildTemplate { } , err
}
template . Roles = append ( template . Roles , guildTemplateRole {
Name : name ,
Permissions : permissionList ( mask ) ,
Color : role . Color ,
Position : role . Position ,
Hoist : role . Hoist ,
Mentionable : role . Mentionable ,
} )
}
channels := make ( [ ] userGuildTemplateChannel , len ( structure . Channels ) )
copy ( channels , structure . Channels )
sort . SliceStable ( channels , func ( i , j int ) bool { return channels [ i ] . Position < channels [ j ] . Position } )
for _ , channel := range channels {
name := strings . TrimSpace ( channel . Name )
if name == "" {
return guildTemplate { } , errors . New ( "channel name must not be empty" )
}
channelType := store . ChannelType ( channel . Type )
switch channelType {
case store . ChannelText , store . ChannelVoice , store . ChannelCategory :
case store . ChannelDM :
return guildTemplate { } , errors . New ( "dm channels are not allowed in a template" )
default :
return guildTemplate { } , fmt . Errorf ( "unknown channel type %q" , channel . Type )
}
template . Channels = append ( template . Channels , guildTemplateChannel {
Name : name ,
Type : channelType ,
Category : channel . Category ,
SlowmodeSeconds : channel . SlowmodeSeconds ,
UserLimit : channel . UserLimit ,
} )
}
return template , nil
}
// permissionList разворачивает маску в список прав — внутренний вид шаблона.
func permissionList ( mask permissions . Permission ) [ ] permissions . Permission {
list := make ( [ ] permissions . Permission , 0 , 8 )
for bit := permissions . Permission ( 1 ) ; bit != 0 && bit <= permissions . Administrator ; bit <<= 1 {
if mask . Has ( bit ) {
list = append ( list , bit )
}
}
return list
}
// parseTemplatePermissions разбирает права роли из structure_json: строка
// названий через «|» (как в API ролей), «NONE» или пустая строка — нет прав;
// числовая строка-маска тоже принимается.
func parseTemplatePermissions ( value string ) ( permissions . Permission , error ) {
trimmed := strings . TrimSpace ( value )
if trimmed == "" || strings . EqualFold ( trimmed , "NONE" ) {
return 0 , nil
}
if mask , err := strconv . ParseUint ( trimmed , 10 , 64 ) ; err == nil {
return permissions . Permission ( mask ) , nil
}
mask , err := permissions . Parse ( trimmed )
if err != nil {
return 0 , err
}
return mask , nil
}
// resolveGuildTemplate выбирает шаблон структуры для нового сервера: встроенный
// по идентификатору каталога или личный по префиксу personal:<id>. Второе
// значение — причина для аудита guild.create (AGENT.md 7.3).
func ( s * Server ) resolveGuildTemplate ( ctx context . Context , user * store . User , templateID string ) ( guildTemplate , string , error ) {
if ! strings . HasPrefix ( templateID , personalTemplatePrefix ) {
template , known := findGuildTemplate ( templateID )
if ! known {
return guildTemplate { } , "" , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "unknown guild template" )
}
return template , "template=" + template . ID , nil
}
id , err := parseID ( "template_id" , strings . TrimPrefix ( templateID , personalTemplatePrefix ) )
if err != nil {
return guildTemplate { } , "" , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "unknown guild template" )
}
stored , err := s . store . GetUserGuildTemplate ( ctx , id )
if err != nil {
if errors . Is ( err , store . ErrNotFound ) {
return guildTemplate { } , "" , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "unknown guild template" )
}
return guildTemplate { } , "" , humaError ( err )
}
// Чужой личный шаблон неотличим от несуществующего (AGENT.md 9.7).
if stored . OwnerID != user . ID {
return guildTemplate { } , "" , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "unknown guild template" )
}
var structure userGuildTemplateStructure
if err := json . Unmarshal ( stored . Structure , & structure ) ; err != nil {
return guildTemplate { } , "" , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "guild template structure is invalid" )
}
template , err := structureToGuildTemplate ( structure )
if err != nil {
return guildTemplate { } , "" , humaErrorStatus ( http . StatusUnprocessableEntity , "validation.failed" , "guild template structure is invalid" )
}
return template , "template=" + personalTemplatePrefix + formatSnowflake ( stored . ID ) , nil
}
// userGuildTemplatePayload описывает личный шаблон для клиента: тот же
// предпросмотр, что у встроенных шаблонов, плюс дата создания и сервер-источник.
func ( s * Server ) userGuildTemplatePayload ( template store . UserGuildTemplate ) guildTemplatePayload {
payload := guildTemplatePayload {
ID : formatSnowflake ( template . ID ) ,
Name : template . Name ,
Description : template . Description ,
Roles : [ ] string { "Администратор" , "Пользователь" } ,
Categories : [ ] string { } ,
CreatedAt : s . store . Timestamp ( template . CreatedAt ) ,
}
if template . SourceGuildID != nil {
payload . SourceGuildID = formatSnowflake ( * template . SourceGuildID )
}
var structure userGuildTemplateStructure
if err := json . Unmarshal ( template . Structure , & structure ) ; err != nil {
// Битый шаблон не должен ломать список: отдаём его без предпросмотра.
return payload
}
fillTemplatePreview ( & payload , structure )
return payload
}
// fillTemplatePreview дополняет предпросмотр по структуре шаблона. Роли
// «Администратор» и «Пользователь» уже перечислены: они создаются для каждого
// сервера (AGENT.md 6.3), поэтому повторно их не добавляем.
func fillTemplatePreview ( payload * guildTemplatePayload , structure userGuildTemplateStructure ) {
seen := make ( map [ string ] bool , len ( payload . Roles ) )
for _ , name := range payload . Roles {
seen [ name ] = true
}
for _ , role := range structure . Roles {
name := strings . TrimSpace ( role . Name )
if name == "" || seen [ name ] {
continue
}
seen [ name ] = true
payload . Roles = append ( payload . Roles , name )
}
categories := make ( map [ string ] bool , 4 )
for _ , channel := range structure . Channels {
switch store . ChannelType ( 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 :
// Личных бесед в шаблонах нет.
}
}
}