2026-09-19 21:50:06 +03:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
|
|
|
|
|
|
"glchat/internal/bootstrap"
|
|
|
|
|
"glchat/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// instancePayload — публичная информация об инстансе (AGENT.md 6.5).
|
|
|
|
|
type instancePayload struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
RegistrationEnabled bool `json:"registration_enabled"`
|
|
|
|
|
AllowGuildCreation bool `json:"allow_guild_creation"`
|
|
|
|
|
VoiceEnabled bool `json:"voice_enabled"`
|
|
|
|
|
MaxGuildsPerUser int `json:"max_guilds_per_user"`
|
|
|
|
|
MaxMembersPerGuild int `json:"max_members_per_guild"`
|
|
|
|
|
MaxMessageLength int `json:"max_message_length"`
|
|
|
|
|
MainGuildID string `json:"main_guild_id,omitempty"`
|
|
|
|
|
UserCount int `json:"user_count"`
|
|
|
|
|
GuildCount int `json:"guild_count"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceOutput struct {
|
|
|
|
|
Body struct {
|
|
|
|
|
Instance instancePayload `json:"instance"`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceGuildPayload struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
OwnerID string `json:"owner_id"`
|
|
|
|
|
OwnerName string `json:"owner_name,omitempty"`
|
|
|
|
|
IsMain bool `json:"is_main"`
|
|
|
|
|
MemberCount int `json:"member_count"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceGuildListOutput struct {
|
|
|
|
|
Body struct {
|
|
|
|
|
Guilds []instanceGuildPayload `json:"guilds"`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceUserPayload struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
DisplayName string `json:"display_name"`
|
|
|
|
|
IsInstanceAdmin bool `json:"is_instance_admin"`
|
|
|
|
|
Badges []string `json:"badges"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceUserListOutput struct {
|
|
|
|
|
Body struct {
|
|
|
|
|
Users []instanceUserPayload `json:"users"`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceSettingsPayload struct {
|
|
|
|
|
RegistrationEnabled bool `json:"registration_enabled"`
|
|
|
|
|
AllowGuildCreation bool `json:"allow_guild_creation"`
|
|
|
|
|
MaxGuildsPerUser int `json:"max_guilds_per_user"`
|
|
|
|
|
MaxMembersPerGuild int `json:"max_members_per_guild"`
|
|
|
|
|
MaxMessageLength int `json:"max_message_length"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type instanceSettingsOutput struct {
|
|
|
|
|
Body struct {
|
|
|
|
|
Settings instanceSettingsPayload `json:"settings"`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// registerInstanceRoutes описывает публичную информацию об инстансе и
|
|
|
|
|
// админ-панель администратора инстанса (AGENT.md 6.5, 7.19).
|
|
|
|
|
func (s *Server) registerInstanceRoutes(api huma.API) {
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "getInstance",
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: "/instance",
|
|
|
|
|
Summary: "Публичная информация об инстансе",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
}, func(ctx context.Context, _ *struct{}) (*instanceOutput, error) {
|
|
|
|
|
payload, err := s.instancePayload(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// Лимиты и число пользователей видны только администратору инстанса.
|
|
|
|
|
if user, _, ok := sessionFromContext(ctx); !ok || !user.IsInstanceAdmin {
|
|
|
|
|
payload.MaxGuildsPerUser = 0
|
|
|
|
|
payload.MaxMembersPerGuild = 0
|
|
|
|
|
payload.UserCount = 0
|
|
|
|
|
payload.GuildCount = 0
|
|
|
|
|
}
|
|
|
|
|
output := &instanceOutput{}
|
|
|
|
|
output.Body.Instance = payload
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "getInstanceSettings",
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: "/instance/settings",
|
|
|
|
|
Summary: "Настройки инстанса (только администратор)",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, _ *struct{}) (*instanceSettingsOutput, error) {
|
|
|
|
|
if _, err := requireInstanceAdmin(ctx); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
settings, err := s.store.InstanceSettings(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
output := &instanceSettingsOutput{}
|
|
|
|
|
output.Body.Settings = instanceSettingsPayload{
|
|
|
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
|
|
|
AllowGuildCreation: settings.AllowGuildCreation,
|
|
|
|
|
MaxGuildsPerUser: settings.MaxGuildsPerUser,
|
|
|
|
|
MaxMembersPerGuild: settings.MaxMembersPerGuild,
|
|
|
|
|
MaxMessageLength: settings.MaxMessageLength,
|
|
|
|
|
}
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "updateInstanceSettings",
|
|
|
|
|
Method: http.MethodPatch,
|
|
|
|
|
Path: "/instance/settings",
|
|
|
|
|
Summary: "Изменить настройки инстанса",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, input *struct {
|
|
|
|
|
Body struct {
|
|
|
|
|
RegistrationEnabled *bool `json:"registration_enabled,omitempty"`
|
|
|
|
|
AllowGuildCreation *bool `json:"allow_guild_creation,omitempty"`
|
|
|
|
|
MaxGuildsPerUser *int `json:"max_guilds_per_user,omitempty" minimum:"1" maximum:"10000"`
|
|
|
|
|
MaxMembersPerGuild *int `json:"max_members_per_guild,omitempty" minimum:"1" maximum:"1000000"`
|
|
|
|
|
MaxMessageLength *int `json:"max_message_length,omitempty" minimum:"1" maximum:"100000"`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
) (*instanceSettingsOutput, error) {
|
|
|
|
|
user, err := requireInstanceAdmin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
updates := map[string]string{}
|
|
|
|
|
if input.Body.RegistrationEnabled != nil {
|
|
|
|
|
updates["registration_enabled"] = strconv.FormatBool(*input.Body.RegistrationEnabled)
|
|
|
|
|
}
|
|
|
|
|
if input.Body.AllowGuildCreation != nil {
|
|
|
|
|
updates["allow_guild_creation"] = strconv.FormatBool(*input.Body.AllowGuildCreation)
|
|
|
|
|
}
|
|
|
|
|
if input.Body.MaxGuildsPerUser != nil {
|
|
|
|
|
updates["max_guilds_per_user"] = strconv.Itoa(*input.Body.MaxGuildsPerUser)
|
|
|
|
|
}
|
|
|
|
|
if input.Body.MaxMembersPerGuild != nil {
|
|
|
|
|
updates["max_members_per_guild"] = strconv.Itoa(*input.Body.MaxMembersPerGuild)
|
|
|
|
|
}
|
|
|
|
|
if input.Body.MaxMessageLength != nil {
|
|
|
|
|
updates["max_message_length"] = strconv.Itoa(*input.Body.MaxMessageLength)
|
|
|
|
|
}
|
|
|
|
|
for key, value := range updates {
|
|
|
|
|
if err := s.store.SetInstanceSetting(ctx, key, value); err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
settings, err := s.store.InstanceSettings(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
s.recordAudit(ctx, user, 0, "instance.settings_update", "instance", nil, "")
|
|
|
|
|
output := &instanceSettingsOutput{}
|
|
|
|
|
output.Body.Settings = instanceSettingsPayload{
|
|
|
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
|
|
|
AllowGuildCreation: settings.AllowGuildCreation,
|
|
|
|
|
MaxGuildsPerUser: settings.MaxGuildsPerUser,
|
|
|
|
|
MaxMembersPerGuild: settings.MaxMembersPerGuild,
|
|
|
|
|
MaxMessageLength: settings.MaxMessageLength,
|
|
|
|
|
}
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "listInstanceGuilds",
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: "/instance/guilds",
|
|
|
|
|
Summary: "Все серверы инстанса",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, _ *struct{}) (*instanceGuildListOutput, error) {
|
|
|
|
|
if _, err := requireInstanceAdmin(ctx); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
guilds, err := s.store.ListAllGuilds(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
output := &instanceGuildListOutput{}
|
|
|
|
|
output.Body.Guilds = make([]instanceGuildPayload, 0, len(guilds))
|
|
|
|
|
for _, guild := range guilds {
|
|
|
|
|
payload := instanceGuildPayload{
|
|
|
|
|
ID: formatSnowflake(guild.ID),
|
|
|
|
|
Name: guild.Name,
|
|
|
|
|
OwnerID: formatSnowflake(guild.OwnerID),
|
|
|
|
|
IsMain: guild.IsMain,
|
|
|
|
|
CreatedAt: guild.CreatedAt.UTC().Format(time.RFC3339),
|
|
|
|
|
}
|
|
|
|
|
if owner, err := s.store.GetUser(ctx, guild.OwnerID); err == nil {
|
|
|
|
|
payload.OwnerName = owner.Username
|
|
|
|
|
}
|
|
|
|
|
if count, err := s.store.CountGuildMembers(ctx, guild.ID); err == nil {
|
|
|
|
|
payload.MemberCount = count
|
|
|
|
|
}
|
|
|
|
|
output.Body.Guilds = append(output.Body.Guilds, payload)
|
|
|
|
|
}
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "adminCreateGuild",
|
|
|
|
|
Method: http.MethodPost,
|
|
|
|
|
Path: "/instance/guilds",
|
|
|
|
|
Summary: "Создать сервер от имени администратора (лимиты обходятся)",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, input *struct {
|
|
|
|
|
Body struct {
|
|
|
|
|
Name string `json:"name" minLength:"1" maxLength:"64"`
|
|
|
|
|
OwnerID string `json:"owner_id,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
) (*instanceGuildListOutput, error) {
|
|
|
|
|
admin, err := requireInstanceAdmin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
owner := admin
|
|
|
|
|
if input.Body.OwnerID != "" {
|
|
|
|
|
ownerID, err := parseID("owner_id", input.Body.OwnerID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
owner, err = s.store.GetUser(ctx, ownerID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
name := strings.TrimSpace(input.Body.Name)
|
|
|
|
|
if name == "" {
|
|
|
|
|
return nil, humaErrorStatus(http.StatusUnprocessableEntity, "validation.failed", "guild name must not be empty")
|
|
|
|
|
}
|
|
|
|
|
// Администратор инстанса создаёт сервер в обход лимитов: причина
|
|
|
|
|
// фиксируется в аудите отдельной записью (AGENT.md 6.5).
|
|
|
|
|
guild, err := s.createGuildAsAdmin(ctx, admin, owner, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
output := &instanceGuildListOutput{}
|
|
|
|
|
output.Body.Guilds = []instanceGuildPayload{{
|
|
|
|
|
ID: formatSnowflake(guild.ID),
|
|
|
|
|
Name: guild.Name,
|
|
|
|
|
OwnerID: formatSnowflake(guild.OwnerID),
|
|
|
|
|
IsMain: guild.IsMain,
|
|
|
|
|
CreatedAt: guild.CreatedAt.UTC().Format(time.RFC3339),
|
|
|
|
|
}}
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "adminDeleteGuild",
|
|
|
|
|
Method: http.MethodDelete,
|
|
|
|
|
Path: "/instance/guilds/{guild_id}",
|
|
|
|
|
Summary: "Удалить сервер (администратор инстанса)",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, input *struct {
|
|
|
|
|
GuildID string `path:"guild_id"`
|
|
|
|
|
},
|
|
|
|
|
) (*okOutput, error) {
|
|
|
|
|
admin, err := requireInstanceAdmin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
guildID, err := parseID("guild_id", input.GuildID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := s.deleteGuild(ctx, admin, guildID); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return newOKOutput(), nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "listInstanceUsers",
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: "/instance/users",
|
|
|
|
|
Summary: "Пользователи инстанса",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, input *struct {
|
|
|
|
|
Limit int `query:"limit" default:"50" minimum:"1" maximum:"200"`
|
|
|
|
|
Offset int `query:"offset" default:"0" minimum:"0"`
|
|
|
|
|
},
|
|
|
|
|
) (*instanceUserListOutput, error) {
|
|
|
|
|
if _, err := requireInstanceAdmin(ctx); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
users, err := s.store.ListUsers(ctx, input.Limit, input.Offset)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
output := &instanceUserListOutput{}
|
|
|
|
|
output.Body.Users = make([]instanceUserPayload, 0, len(users))
|
|
|
|
|
for _, user := range users {
|
|
|
|
|
badges := user.Badges
|
|
|
|
|
if badges == nil {
|
|
|
|
|
badges = []string{}
|
|
|
|
|
}
|
|
|
|
|
output.Body.Users = append(output.Body.Users, instanceUserPayload{
|
|
|
|
|
ID: formatSnowflake(user.ID),
|
|
|
|
|
Username: user.Username,
|
|
|
|
|
DisplayName: user.DisplayName,
|
|
|
|
|
IsInstanceAdmin: user.IsInstanceAdmin,
|
|
|
|
|
Badges: badges,
|
|
|
|
|
CreatedAt: user.CreatedAt.UTC().Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "setInstanceAdmin",
|
|
|
|
|
Method: http.MethodPost,
|
|
|
|
|
Path: "/instance/users/{user_id}/admin",
|
|
|
|
|
Summary: "Выдать или снять права администратора инстанса",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, input *struct {
|
|
|
|
|
UserID string `path:"user_id"`
|
|
|
|
|
Body struct {
|
|
|
|
|
Admin bool `json:"admin"`
|
|
|
|
|
// StepUpPassword подтверждает действие: смена администраторов —
|
|
|
|
|
// чувствительная операция (AGENT.md 7.1).
|
|
|
|
|
StepUpPassword string `json:"step_up_password,omitempty"`
|
|
|
|
|
StepUpTOTP string `json:"step_up_totp,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
) (*userOutput, error) {
|
|
|
|
|
admin, session, err := requireUser(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if !admin.IsInstanceAdmin {
|
|
|
|
|
return nil, humaErrorStatus(http.StatusForbidden, "instance.admin_required", "instance administrator rights required")
|
|
|
|
|
}
|
|
|
|
|
if err := s.auth.RequireStepUp(ctx, admin, session, input.Body.StepUpPassword, input.Body.StepUpTOTP); err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
userID, err := parseID("user_id", input.UserID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if userID == admin.ID && !input.Body.Admin {
|
|
|
|
|
return nil, humaErrorStatus(http.StatusUnprocessableEntity, "validation.failed", "you cannot revoke your own administrator rights")
|
|
|
|
|
}
|
|
|
|
|
if err := s.store.SetInstanceAdmin(ctx, userID, input.Body.Admin); err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
updated, err := s.store.GetUser(ctx, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
action := "instance.admin_grant"
|
|
|
|
|
if !input.Body.Admin {
|
|
|
|
|
action = "instance.admin_revoke"
|
|
|
|
|
}
|
|
|
|
|
s.recordAudit(ctx, admin, 0, action, "user", &userID, "")
|
|
|
|
|
s.dispatchUserUpdate(updated)
|
|
|
|
|
output := &userOutput{}
|
2026-09-19 22:03:46 +03:00
|
|
|
output.Body.User = s.profileFromUser(ctx, updated, false)
|
2026-09-19 21:50:06 +03:00
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
huma.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "listInstanceAudit",
|
|
|
|
|
Method: http.MethodGet,
|
|
|
|
|
Path: "/instance/audit",
|
|
|
|
|
Summary: "Журнал действий администраторов инстанса",
|
|
|
|
|
Tags: []string{"Instance"},
|
|
|
|
|
Security: []map[string][]string{{"sessionCookie": {}}, {"bearerAuth": {}}},
|
|
|
|
|
}, func(ctx context.Context, input *struct {
|
|
|
|
|
Limit int `query:"limit" default:"50" minimum:"1" maximum:"200"`
|
|
|
|
|
},
|
|
|
|
|
) (*auditListOutput, error) {
|
|
|
|
|
if _, err := requireInstanceAdmin(ctx); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
entries, err := s.store.ListInstanceAudit(ctx, input.Limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
output := &auditListOutput{}
|
|
|
|
|
output.Body.Entries = auditPayloads(entries)
|
|
|
|
|
return output, nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// instancePayload собирает публичные сведения об инстансе.
|
|
|
|
|
func (s *Server) instancePayload(ctx context.Context) (instancePayload, error) {
|
|
|
|
|
settings, err := s.store.InstanceSettings(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return instancePayload{}, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
users, err := s.store.CountUsers(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return instancePayload{}, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
guilds, err := s.store.ListAllGuilds(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return instancePayload{}, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
payload := instancePayload{
|
|
|
|
|
Name: s.cfg.InstanceName,
|
|
|
|
|
Version: s.cfg.Version,
|
|
|
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
|
|
|
AllowGuildCreation: settings.AllowGuildCreation,
|
|
|
|
|
MaxGuildsPerUser: settings.MaxGuildsPerUser,
|
|
|
|
|
MaxMembersPerGuild: settings.MaxMembersPerGuild,
|
|
|
|
|
MaxMessageLength: settings.MaxMessageLength,
|
|
|
|
|
UserCount: users,
|
|
|
|
|
GuildCount: len(guilds),
|
|
|
|
|
}
|
|
|
|
|
if settings.MainGuildID != 0 {
|
|
|
|
|
payload.MainGuildID = formatSnowflake(settings.MainGuildID)
|
|
|
|
|
}
|
|
|
|
|
return payload, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createGuildAsAdmin создаёт сервер в обход лимитов и фиксирует это в аудите.
|
|
|
|
|
func (s *Server) createGuildAsAdmin(ctx context.Context, admin, owner *store.User, name string) (*store.Guild, error) {
|
|
|
|
|
settings, err := s.store.InstanceSettings(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
guild, err := s.store.CreateGuild(ctx, store.CreateGuildParams{
|
|
|
|
|
Name: name,
|
|
|
|
|
OwnerID: owner.ID,
|
|
|
|
|
IsMain: settings.MainGuildID == 0,
|
|
|
|
|
IsDiscoverable: false,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := bootstrap.SeedGuildDefaults(ctx, s.store, guild, owner); err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := s.store.CreateChannel(ctx, store.CreateChannelParams{
|
|
|
|
|
GuildID: &guild.ID, Type: store.ChannelText, Name: "общий", Position: 0,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
if guild.IsMain {
|
|
|
|
|
if err := s.store.SetInstanceSetting(ctx, "main_guild_id", strconv.FormatUint(guild.ID, 10)); err != nil {
|
|
|
|
|
return nil, humaError(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.recordAudit(ctx, admin, guild.ID, "guild.create", "guild", &guild.ID, "created by instance admin")
|
|
|
|
|
s.recordAudit(ctx, admin, guild.ID, "limits.bypass", "guild", &guild.ID, "instance admin bypassed guild limits")
|
|
|
|
|
if s.gateway != nil {
|
|
|
|
|
s.gateway.SendToUser(owner.ID, "GUILD_CREATE", map[string]any{"guild_id": formatSnowflake(guild.ID)})
|
|
|
|
|
}
|
|
|
|
|
return guild, nil
|
|
|
|
|
}
|