295 lines
8.8 KiB
Go
295 lines
8.8 KiB
Go
|
|
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
|
||
|
|
}
|