// Package permissions — ядро вычисления прав glchat (AGENT.md 6.2, 7.4). // Любая проверка прав на сервере обязана идти через этот пакет. package permissions import ( "context" "fmt" "strings" ) // Permission — битовая маска прав (uint64, AGENT.md 6.2). type Permission uint64 const ( ViewGuild Permission = 1 << iota ViewChannel ReadMessageHistory SendMessages AttachFiles EmbedLinks AddReactions UseExternalEmojis MentionEveryone ManageMessages ConnectVoice Speak Video ScreenShare UseSoundboard MuteMembers DeafenMembers MoveMembers ManageChannels ManageChannelStatus ManageChannelDescription ManageChannelBackground ManageChannelPermissions ManageGuild ManageRoles ManageEmojis ManageSounds UseWebhooks ManageWebhooks CreateInvites KickMembers BanMembers TimeoutMembers ViewAuditLog ManageNicknames ChangeNickname Administrator ) // AllPermissions — маска всех прав. const AllPermissions Permission = (1 << 37) - 1 var permissionNames = map[Permission]string{ ViewGuild: "VIEW_GUILD", ViewChannel: "VIEW_CHANNEL", ReadMessageHistory: "READ_MESSAGE_HISTORY", SendMessages: "SEND_MESSAGES", AttachFiles: "ATTACH_FILES", EmbedLinks: "EMBED_LINKS", AddReactions: "ADD_REACTIONS", UseExternalEmojis: "USE_EXTERNAL_EMOJIS", MentionEveryone: "MENTION_EVERYONE", ManageMessages: "MANAGE_MESSAGES", ConnectVoice: "CONNECT_VOICE", Speak: "SPEAK", Video: "VIDEO", ScreenShare: "SCREEN_SHARE", UseSoundboard: "USE_SOUNDBOARD", MuteMembers: "MUTE_MEMBERS", DeafenMembers: "DEAFEN_MEMBERS", MoveMembers: "MOVE_MEMBERS", ManageChannels: "MANAGE_CHANNELS", ManageChannelStatus: "MANAGE_CHANNEL_STATUS", ManageChannelDescription: "MANAGE_CHANNEL_DESCRIPTION", ManageChannelBackground: "MANAGE_CHANNEL_BACKGROUND", ManageChannelPermissions: "MANAGE_CHANNEL_PERMISSIONS", ManageGuild: "MANAGE_GUILD", ManageRoles: "MANAGE_ROLES", ManageEmojis: "MANAGE_EMOJIS", ManageSounds: "MANAGE_SOUNDS", UseWebhooks: "USE_WEBHOOKS", ManageWebhooks: "MANAGE_WEBHOOKS", CreateInvites: "CREATE_INVITES", KickMembers: "KICK_MEMBERS", BanMembers: "BAN_MEMBERS", TimeoutMembers: "TIMEOUT_MEMBERS", ViewAuditLog: "VIEW_AUDIT_LOG", ManageNicknames: "MANAGE_NICKNAMES", ChangeNickname: "CHANGE_NICKNAME", Administrator: "ADMINISTRATOR", } func (p Permission) Has(required Permission) bool { return p&required == required } // Any сообщает, что установлен хотя бы один бит из маски. func (p Permission) Any(mask Permission) bool { return p&mask != 0 } func (p Permission) String() string { if p == 0 { return "NONE" } names := make([]string, 0, 4) for bit := Permission(1); bit != 0 && bit <= Administrator; bit <<= 1 { if p&bit != 0 { names = append(names, permissionNames[bit]) } } return strings.Join(names, "|") } // AllPermissionsList возвращает все права по порядку битов (для API и UI). func AllPermissionsList() []Permission { list := make([]Permission, 0, 37) for bit := ViewGuild; bit <= Administrator; bit <<= 1 { list = append(list, bit) } return list } // Name возвращает строковое имя права. func Name(permission Permission) string { if name, ok := permissionNames[permission]; ok { return name } return fmt.Sprintf("UNKNOWN_%d", uint64(permission)) } // ByName ищет право по имени (нужно API и импорту шаблонов). func ByName(name string) (Permission, bool) { for permission, candidate := range permissionNames { if candidate == strings.ToUpper(strings.TrimSpace(name)) { return permission, true } } return 0, false } // DefaultUserPermissions — права роли «Пользователь» при создании сервера // (AGENT.md 6.3). const DefaultUserPermissions = ViewGuild | ViewChannel | ReadMessageHistory | SendMessages | AttachFiles | AddReactions | ConnectVoice | Speak // Resolved — результат вычисления прав для контекста сервера и комнаты. type Resolved struct { // Guild — права на уровне сервера (без учёта оверрайдов комнаты). Guild Permission // Channel — права с учётом оверрайдов (только для конкретной комнаты). Channel Permission // IsOwner — пользователь владеет сервером: неявный ADMINISTRATOR (AGENT.md 6.2). IsOwner bool // IsInstanceAdmin — глобальная роль «Администратор сервера» (AGENT.md 7.19). IsInstanceAdmin bool // IsMember — состоит ли пользователь в сервере. Не участник не получает // прав роли @user: сервер для него невидим (AGENT.md 6.2). IsMember bool // TimedOut — участник в тайм-ауте: отправка сообщений запрещена (AGENT.md 7.10). TimedOut bool } // Has — есть ли право на сервере. ADMINISTRATOR даёт все права (AGENT.md 6.2). func (r Resolved) Has(permission Permission) bool { if r.IsInstanceAdmin { return true } if !r.IsMember { return false } if r.Guild.Has(Administrator) { return true } return r.Guild.Has(permission) } // Can — есть ли право в комнате (с учётом тайм-аута и оверрайдов). func (r Resolved) Can(permission Permission) bool { if r.IsInstanceAdmin { return true } if !r.IsMember { return false } if r.Channel.Has(Administrator) { return true } if !r.Channel.Has(permission) { return false } if r.TimedOut && permission.Any(SendMessages|AddReactions|AttachFiles|Speak) { return false } return true } // CanViewChannel — может ли пользователь видеть комнату. Требуется именно // VIEW_CHANNEL: CONNECT_VOICE сам по себе комнату в списке не показывает // (текстовая комната без VIEW_CHANNEL скрыта, AGENT.md 7.5). func (r Resolved) CanViewChannel() bool { if r.IsInstanceAdmin { return true } if !r.IsMember { return false } if r.Channel.Has(Administrator) { return true } return r.Channel.Has(ViewChannel) } // Names возвращает список названий установленных прав: клиенту удобнее // работать со списком, чем с битовой маской (AGENT.md 8.2). func Names(permission Permission) []string { names := make([]string, 0, 8) for bit := Permission(1); bit != 0 && bit <= Administrator; bit <<= 1 { if permission&bit != 0 { names = append(names, permissionNames[bit]) } } return names } // Parse разбирает строку с названиями прав: "VIEW_CHANNEL|SEND_MESSAGES", // "VIEW_CHANNEL,SEND_MESSAGES" или через пробел. Пустая строка — нет прав. func Parse(value string) (Permission, error) { normalized := strings.NewReplacer(",", " ", "|", " ").Replace(value) var result Permission for _, name := range strings.Fields(normalized) { permission, ok := ByName(name) if !ok { return 0, fmt.Errorf("unknown permission %q", name) } result |= permission } return result, nil } // Source — данные для расчёта прав: роли сервера и роли участника. type Source interface { GuildRoles(ctx context.Context, guildID uint64) ([]RoleData, error) MemberRoleIDs(ctx context.Context, guildID, userID uint64) ([]uint64, error) // IsMember отвечает, состоит ли пользователь в сервере. IsMember(ctx context.Context, guildID, userID uint64) (bool, error) ChannelOverrides(ctx context.Context, channelID uint64) ([]OverrideData, error) GuildOwnerID(ctx context.Context, guildID uint64) (uint64, error) MemberTimeout(ctx context.Context, guildID, userID uint64) (bool, error) } // RoleData и OverrideData описывают входные данные расчёта без зависимости // пакета прав от пакета хранения. type RoleData struct { ID uint64 Permissions Permission IsDefault bool Position int } type OverrideData struct { TargetType string // role | user TargetID uint64 Allow Permission Deny Permission } // Calculator вычисляет права и кэширует результат (AGENT.md 9.1). type Calculator struct { source Source cache *Cache } func NewCalculator(source Source) *Calculator { return &Calculator{source: source, cache: NewCache(4096)} } // InvalidateGuild сбрасывает кэш сервера при изменении ролей или оверрайдов. func (c *Calculator) InvalidateGuild(guildID uint64) { c.cache.InvalidateGuild(guildID) } // InvalidateChannel сбрасывает кэш комнаты. func (c *Calculator) InvalidateChannel(channelID uint64) { c.cache.InvalidateChannel(channelID) } // InvalidateAll полностью очищает кэш (после импорта или массовых изменений). func (c *Calculator) InvalidateAll() { c.cache.Clear() } // Guild вычисляет права пользователя на сервере. func (c *Calculator) Guild(ctx context.Context, guildID, userID uint64, instanceAdmin bool) (Resolved, error) { if cached, ok := c.cache.GetGuild(guildID, userID); ok { cached.IsInstanceAdmin = instanceAdmin return cached, nil } roles, err := c.source.GuildRoles(ctx, guildID) if err != nil { return Resolved{}, fmt.Errorf("load guild roles: %w", err) } memberRoleIDs, err := c.source.MemberRoleIDs(ctx, guildID, userID) if err != nil { return Resolved{}, fmt.Errorf("load member roles: %w", err) } ownerID, err := c.source.GuildOwnerID(ctx, guildID) if err != nil { return Resolved{}, fmt.Errorf("load guild owner: %w", err) } resolved := Resolved{IsOwner: ownerID == userID, IsInstanceAdmin: instanceAdmin} resolved.IsMember = resolved.IsOwner if !resolved.IsMember { resolved.IsMember, err = c.source.IsMember(ctx, guildID, userID) if err != nil { return Resolved{}, fmt.Errorf("load guild membership: %w", err) } } if !resolved.IsMember { // Не участник: прав нет вовсе, роль @user на него не действует. c.cache.PutGuild(guildID, userID, resolved) return resolved, nil } assigned := make(map[uint64]bool, len(memberRoleIDs)) for _, roleID := range memberRoleIDs { assigned[roleID] = true } for _, role := range roles { if role.IsDefault || assigned[role.ID] { resolved.Guild |= role.Permissions } } if resolved.IsOwner { // Владелец сервера имеет ADMINISTRATOR неявно и не может его лишиться. resolved.Guild |= Administrator } timedOut, err := c.source.MemberTimeout(ctx, guildID, userID) if err != nil { return Resolved{}, fmt.Errorf("load member timeout: %w", err) } resolved.TimedOut = timedOut c.cache.PutGuild(guildID, userID, resolved) return resolved, nil } // RequireGuild проверяет право на сервере и возвращает ошибку доступа. func RequireGuild(resolved Resolved, permission Permission) error { if !resolved.Has(permission) { return fmt.Errorf("%w: %s", ErrDenied, Name(permission)) } return nil } // RequireChannel проверяет право в комнате и возвращает ошибку доступа. func RequireChannel(resolved Resolved, permission Permission) error { if !resolved.Can(permission) { return fmt.Errorf("%w: %s", ErrDenied, Name(permission)) } return nil } // ErrDenied — единая ошибка отсутствия прав (на HTTP-слое → perm.denied). var ErrDenied = fmt.Errorf("permission denied") // HigherThan сообщает, что роль a выше роли b (AGENT.md 7.4). func HigherThan(a, b RoleData) bool { return a.Position > b.Position } // OutranksRole проверяет, что актор может управлять ролью: его высшая роль // должна быть выше целевой, владелец — выше всех (AGENT.md 7.4). func OutranksRole(actorRoles []RoleData, targetRole RoleData, isOwner bool) bool { if isOwner { return true } for _, role := range actorRoles { if role.Position > targetRole.Position { return true } } return false } // OutranksMember проверяет иерархию участников: нельзя управлять тем, у кого // высшая роль не ниже роли актора (владелец — выше всех). func OutranksMember(actorRoles, targetRoles []RoleData, actorIsOwner, targetIsOwner bool) bool { if actorIsOwner { return !targetIsOwner || actorIsOwner } if targetIsOwner { return false } actorTop := topPosition(actorRoles) targetTop := topPosition(targetRoles) return actorTop > targetTop } func topPosition(roles []RoleData) int { top := 0 for _, role := range roles { if role.Position > top { top = role.Position } } return top }