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 }