Files
glchat/internal/store/roles.go
T

295 lines
8.8 KiB
Go
Raw Normal View History

package store
import (
"context"
"database/sql"
"strings"
"time"
)
type Role struct {
ID uint64
GuildID uint64
Name string
Color int64
Position int
Permissions uint64
IsDefault bool
Mentionable bool
Hoist bool
CosmeticFrameID *uint64
CosmeticBadgeID *uint64
NickColor *int64
NickEffect string
CreatedAt time.Time
}
// CreateRoleParams — параметры роли. Пермишены хранятся как в SQLite (int64),
// а в модели — uint64: конвертация в одном месте.
type CreateRoleParams struct {
ID uint64
GuildID uint64
Name string
Color int64
Position int
Permissions uint64
IsDefault bool
Mentionable bool
Hoist bool
NickEffect string
}
const roleColumns = `id, guild_id, name, color, position, permissions, is_default,
mentionable, hoist, cosmetic_frame_id, cosmetic_badge_id, nick_color, nick_effect, created_at`
func (s *Store) CreateRole(ctx context.Context, params CreateRoleParams) (*Role, error) {
if params.ID == 0 {
params.ID = s.NextID()
}
if params.NickEffect == "" {
params.NickEffect = "none"
}
_, err := s.writer.ExecContext(ctx, `
INSERT INTO roles (id, guild_id, name, color, position, permissions, is_default, mentionable, hoist, nick_effect, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
int64(params.ID), int64(params.GuildID), params.Name, params.Color, params.Position,
int64(params.Permissions), boolToInt(params.IsDefault), boolToInt(params.Mentionable),
boolToInt(params.Hoist), params.NickEffect, s.Now())
if err != nil {
if isUniqueViolation(err) {
return nil, ErrConflict
}
return nil, err
}
return s.GetRole(ctx, params.ID)
}
func (s *Store) GetRole(ctx context.Context, id uint64) (*Role, error) {
return scanRole(s.reader.QueryRowContext(ctx, `SELECT `+roleColumns+` FROM roles WHERE id = ?`, int64(id)))
}
// ListGuildRoles возвращает роли сервера по убыванию позиции (AGENT.md 7.4).
func (s *Store) ListGuildRoles(ctx context.Context, guildID uint64) ([]Role, error) {
rows, err := s.reader.QueryContext(ctx,
`SELECT `+roleColumns+` FROM roles WHERE guild_id = ? ORDER BY position DESC, id`, int64(guildID))
if err != nil {
return nil, err
}
defer rows.Close()
roles := make([]Role, 0, 4)
for rows.Next() {
role, err := scanRole(rows)
if err != nil {
return nil, err
}
roles = append(roles, *role)
}
return roles, rows.Err()
}
// DefaultRole возвращает роль @user (is_default) — базу для вычисления прав.
func (s *Store) DefaultRole(ctx context.Context, guildID uint64) (*Role, error) {
return scanRole(s.reader.QueryRowContext(ctx,
`SELECT `+roleColumns+` FROM roles WHERE guild_id = ? AND is_default = 1 ORDER BY position LIMIT 1`,
int64(guildID)))
}
type UpdateRoleParams struct {
Name *string
Color *int64
Position *int
Permissions *uint64
Mentionable *bool
Hoist *bool
CosmeticFrameID *uint64
CosmeticBadgeID *uint64
NickColor *int64
NickEffect *string
ClearFrame bool
ClearBadge bool
ClearNickColor bool
}
func (s *Store) UpdateRole(ctx context.Context, id uint64, params UpdateRoleParams) (*Role, error) {
sets := []string{}
args := []any{}
if params.Name != nil {
sets = append(sets, "name = ?")
args = append(args, *params.Name)
}
if params.Color != nil {
sets = append(sets, "color = ?")
args = append(args, *params.Color)
}
if params.Position != nil {
sets = append(sets, "position = ?")
args = append(args, *params.Position)
}
if params.Permissions != nil {
sets = append(sets, "permissions = ?")
args = append(args, int64(*params.Permissions))
}
if params.Mentionable != nil {
sets = append(sets, "mentionable = ?")
args = append(args, boolToInt(*params.Mentionable))
}
if params.Hoist != nil {
sets = append(sets, "hoist = ?")
args = append(args, boolToInt(*params.Hoist))
}
if params.CosmeticFrameID != nil {
sets = append(sets, "cosmetic_frame_id = ?")
args = append(args, int64(*params.CosmeticFrameID))
}
if params.ClearFrame {
sets = append(sets, "cosmetic_frame_id = NULL")
}
if params.CosmeticBadgeID != nil {
sets = append(sets, "cosmetic_badge_id = ?")
args = append(args, int64(*params.CosmeticBadgeID))
}
if params.ClearBadge {
sets = append(sets, "cosmetic_badge_id = NULL")
}
if params.NickColor != nil {
sets = append(sets, "nick_color = ?")
args = append(args, *params.NickColor)
}
if params.ClearNickColor {
sets = append(sets, "nick_color = NULL")
}
if params.NickEffect != nil {
sets = append(sets, "nick_effect = ?")
args = append(args, *params.NickEffect)
}
if len(sets) == 0 {
return s.GetRole(ctx, id)
}
args = append(args, int64(id))
if _, err := s.writer.ExecContext(ctx, buildQuery(updateRoleTemplate, strings.Join(sets, ", ")), args...); err != nil {
return nil, err
}
return s.GetRole(ctx, id)
}
func (s *Store) DeleteRole(ctx context.Context, id uint64) error {
_, err := s.writer.ExecContext(ctx, `DELETE FROM roles WHERE id = ?`, int64(id))
return err
}
// AssignRole выдаёт роль участнику (идемпотентно).
func (s *Store) AssignRole(ctx context.Context, guildID, userID, roleID uint64) error {
_, err := s.writer.ExecContext(ctx, `
INSERT INTO member_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)
ON CONFLICT DO NOTHING`, int64(guildID), int64(userID), int64(roleID))
return err
}
func (s *Store) RemoveRole(ctx context.Context, guildID, userID, roleID uint64) error {
_, err := s.writer.ExecContext(ctx,
`DELETE FROM member_roles WHERE guild_id = ? AND user_id = ? AND role_id = ?`,
int64(guildID), int64(userID), int64(roleID))
return err
}
// MemberRoles возвращает роли участника сервера.
func (s *Store) MemberRoles(ctx context.Context, guildID, userID uint64) ([]Role, error) {
rows, err := s.reader.QueryContext(ctx, buildQuery(selectMemberRolesTemplate, prefixColumns("r", roleColumns)),
int64(guildID), int64(userID))
if err != nil {
return nil, err
}
defer rows.Close()
roles := make([]Role, 0, 4)
for rows.Next() {
role, err := scanRole(rows)
if err != nil {
return nil, err
}
roles = append(roles, *role)
}
return roles, rows.Err()
}
// MemberRoleIDs возвращает идентификаторы ролей участника (для READY).
func (s *Store) MemberRoleIDs(ctx context.Context, guildID, userID uint64) ([]uint64, error) {
rows, err := s.reader.QueryContext(ctx,
`SELECT role_id FROM member_roles WHERE guild_id = ? AND user_id = ?`, int64(guildID), int64(userID))
if err != nil {
return nil, err
}
defer rows.Close()
ids := make([]uint64, 0, 4)
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, uint64(id))
}
return ids, rows.Err()
}
// RoleMemberCount считает участников с ролью (нужно для защиты от потери
// управления сервером, AGENT.md 6.3).
func (s *Store) RoleMemberCount(ctx context.Context, roleID uint64) (int, error) {
var count int
err := s.reader.QueryRowContext(ctx, `SELECT COUNT(*) FROM member_roles WHERE role_id = ?`, int64(roleID)).Scan(&count)
return count, err
}
// GuildRolesWithManageRoles возвращает роли, дающие право управлять ролями.
func (s *Store) GuildRolesWithManageRoles(ctx context.Context, guildID uint64, permission uint64) ([]Role, error) {
rows, err := s.reader.QueryContext(ctx, `
SELECT `+roleColumns+` FROM roles
WHERE guild_id = ? AND (permissions & ?) <> 0`, int64(guildID), int64(permission))
if err != nil {
return nil, err
}
defer rows.Close()
roles := make([]Role, 0, 2)
for rows.Next() {
role, err := scanRole(rows)
if err != nil {
return nil, err
}
roles = append(roles, *role)
}
return roles, rows.Err()
}
func scanRole(scanner interface{ Scan(...any) error }) (*Role, error) {
var (
role Role
permissions int64
isDefault int
mentionable int
hoist int
frameID sql.NullInt64
badgeID sql.NullInt64
nickColor sql.NullInt64
createdAt string
)
err := scanner.Scan(&role.ID, &role.GuildID, &role.Name, &role.Color, &role.Position, &permissions,
&isDefault, &mentionable, &hoist, &frameID, &badgeID, &nickColor, &role.NickEffect, &createdAt)
if err != nil {
return nil, mapError(err)
}
role.Permissions = uint64(permissions)
role.IsDefault = isDefault == 1
role.Mentionable = mentionable == 1
role.Hoist = hoist == 1
role.CosmeticFrameID = optionalID(frameID)
role.CosmeticBadgeID = optionalID(badgeID)
if nickColor.Valid {
value := nickColor.Int64
role.NickColor = &value
}
role.CreatedAt = parseTimestamp(createdAt)
return &role, nil
}