2026-09-19 21:24:17 +03:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Guild struct {
|
|
|
|
|
ID uint64
|
|
|
|
|
Name string
|
|
|
|
|
OwnerID uint64
|
|
|
|
|
IconFileID *uint64
|
|
|
|
|
BannerFileID *uint64
|
|
|
|
|
SplashFileID *uint64
|
|
|
|
|
AccentColor int64
|
|
|
|
|
Description string
|
|
|
|
|
IsMain bool
|
|
|
|
|
IsDiscoverable bool
|
|
|
|
|
Limits map[string]int
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GuildMember struct {
|
|
|
|
|
GuildID uint64
|
|
|
|
|
UserID uint64
|
|
|
|
|
Nickname string
|
|
|
|
|
JoinedAt time.Time
|
|
|
|
|
TimeoutUntil *time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateGuildParams — параметры создания сервера. Роли по умолчанию создаёт
|
|
|
|
|
// вызывающий код (bootstrap или создатель сервера), чтобы можно было
|
|
|
|
|
// применить шаблон оформления (AGENT.md 7.3).
|
|
|
|
|
type CreateGuildParams struct {
|
|
|
|
|
ID uint64
|
|
|
|
|
Name string
|
|
|
|
|
OwnerID uint64
|
|
|
|
|
IsMain bool
|
|
|
|
|
IsDiscoverable bool
|
|
|
|
|
Description string
|
|
|
|
|
AccentColor int64
|
|
|
|
|
Limits map[string]int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const guildColumns = `id, name, owner_id, icon_file_id, banner_file_id, splash_file_id,
|
|
|
|
|
accent_color, description, is_main, is_discoverable, limits_json, created_at`
|
|
|
|
|
|
|
|
|
|
func (s *Store) CreateGuild(ctx context.Context, params CreateGuildParams) (*Guild, error) {
|
|
|
|
|
if params.ID == 0 {
|
|
|
|
|
params.ID = s.NextID()
|
|
|
|
|
}
|
|
|
|
|
limits := "{}"
|
|
|
|
|
if len(params.Limits) > 0 {
|
|
|
|
|
if encoded, err := json.Marshal(params.Limits); err == nil {
|
|
|
|
|
limits = string(encoded)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_, err := s.writer.ExecContext(ctx, `
|
|
|
|
|
INSERT INTO guilds (id, name, owner_id, description, accent_color, is_main, is_discoverable, limits_json, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
int64(params.ID), params.Name, int64(params.OwnerID), params.Description, params.AccentColor,
|
|
|
|
|
boolToInt(params.IsMain), boolToInt(params.IsDiscoverable), limits, s.Now())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return s.GetGuild(ctx, params.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) GetGuild(ctx context.Context, id uint64) (*Guild, error) {
|
|
|
|
|
return scanGuild(s.reader.QueryRowContext(ctx, `SELECT `+guildColumns+` FROM guilds WHERE id = ?`, int64(id)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 01:12:01 +03:00
|
|
|
// GetAdminGuild возвращает сервер, созданный установщиком (справочно).
|
|
|
|
|
func (s *Store) GetAdminGuild(ctx context.Context) (*Guild, error) {
|
|
|
|
|
settings, err := s.InstanceSettings(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if settings.MainGuildID == 0 {
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return s.GetGuild(ctx, settings.MainGuildID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 21:24:17 +03:00
|
|
|
func (s *Store) GetMainGuild(ctx context.Context) (*Guild, error) {
|
|
|
|
|
return scanGuild(s.reader.QueryRowContext(ctx,
|
|
|
|
|
`SELECT `+guildColumns+` FROM guilds WHERE is_main = 1 ORDER BY id LIMIT 1`))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UpdateGuildParams struct {
|
|
|
|
|
Name *string
|
|
|
|
|
Description *string
|
|
|
|
|
AccentColor *int64
|
|
|
|
|
IconFileID *uint64
|
|
|
|
|
BannerFileID *uint64
|
|
|
|
|
SplashFileID *uint64
|
|
|
|
|
IsDiscoverable *bool
|
|
|
|
|
Limits map[string]int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) UpdateGuild(ctx context.Context, id uint64, params UpdateGuildParams) (*Guild, error) {
|
|
|
|
|
var (
|
|
|
|
|
sets []string
|
|
|
|
|
args []any
|
|
|
|
|
)
|
|
|
|
|
if params.Name != nil {
|
|
|
|
|
sets = append(sets, "name = ?")
|
|
|
|
|
args = append(args, *params.Name)
|
|
|
|
|
}
|
|
|
|
|
if params.Description != nil {
|
|
|
|
|
sets = append(sets, "description = ?")
|
|
|
|
|
args = append(args, *params.Description)
|
|
|
|
|
}
|
|
|
|
|
if params.AccentColor != nil {
|
|
|
|
|
sets = append(sets, "accent_color = ?")
|
|
|
|
|
args = append(args, *params.AccentColor)
|
|
|
|
|
}
|
|
|
|
|
if params.IconFileID != nil {
|
|
|
|
|
sets = append(sets, "icon_file_id = ?")
|
|
|
|
|
args = append(args, int64(*params.IconFileID))
|
|
|
|
|
}
|
|
|
|
|
if params.BannerFileID != nil {
|
|
|
|
|
sets = append(sets, "banner_file_id = ?")
|
|
|
|
|
args = append(args, int64(*params.BannerFileID))
|
|
|
|
|
}
|
|
|
|
|
if params.SplashFileID != nil {
|
|
|
|
|
sets = append(sets, "splash_file_id = ?")
|
|
|
|
|
args = append(args, int64(*params.SplashFileID))
|
|
|
|
|
}
|
|
|
|
|
if params.IsDiscoverable != nil {
|
|
|
|
|
sets = append(sets, "is_discoverable = ?")
|
|
|
|
|
args = append(args, boolToInt(*params.IsDiscoverable))
|
|
|
|
|
}
|
|
|
|
|
if params.Limits != nil {
|
|
|
|
|
encoded, err := json.Marshal(params.Limits)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
sets = append(sets, "limits_json = ?")
|
|
|
|
|
args = append(args, string(encoded))
|
|
|
|
|
}
|
|
|
|
|
if len(sets) == 0 {
|
|
|
|
|
return s.GetGuild(ctx, id)
|
|
|
|
|
}
|
|
|
|
|
args = append(args, int64(id))
|
|
|
|
|
result, err := s.writer.ExecContext(ctx, buildQuery(updateGuildTemplate, joinSets(sets)), args...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if affected, err := result.RowsAffected(); err == nil && affected == 0 {
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return s.GetGuild(ctx, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) DeleteGuild(ctx context.Context, id uint64) error {
|
|
|
|
|
_, err := s.writer.ExecContext(ctx, `DELETE FROM guilds WHERE id = ?`, int64(id))
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListGuildsForUser возвращает серверы пользователя (участие).
|
|
|
|
|
func (s *Store) ListGuildsForUser(ctx context.Context, userID uint64) ([]Guild, error) {
|
|
|
|
|
rows, err := s.reader.QueryContext(ctx, buildQuery(selectGuildsForUserTemplate, prefixColumns("g", guildColumns)), int64(userID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return collectGuilds(rows)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListAllGuilds используется инстанс-админом (AGENT.md 7.19).
|
|
|
|
|
func (s *Store) ListAllGuilds(ctx context.Context) ([]Guild, error) {
|
|
|
|
|
rows, err := s.reader.QueryContext(ctx, `SELECT `+guildColumns+` FROM guilds ORDER BY id`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return collectGuilds(rows)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) CountGuildsForUser(ctx context.Context, userID uint64) (int, error) {
|
|
|
|
|
var count int
|
|
|
|
|
err := s.reader.QueryRowContext(ctx, `SELECT COUNT(*) FROM guild_members WHERE user_id = ?`, int64(userID)).Scan(&count)
|
|
|
|
|
return count, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) CountGuildMembers(ctx context.Context, guildID uint64) (int, error) {
|
|
|
|
|
var count int
|
|
|
|
|
err := s.reader.QueryRowContext(ctx, `SELECT COUNT(*) FROM guild_members WHERE guild_id = ?`, int64(guildID)).Scan(&count)
|
|
|
|
|
return count, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) AddGuildMember(ctx context.Context, guildID, userID uint64, nickname string) (*GuildMember, error) {
|
|
|
|
|
_, err := s.writer.ExecContext(ctx, `
|
|
|
|
|
INSERT INTO guild_members (guild_id, user_id, nickname, joined_at) VALUES (?, ?, ?, ?)
|
|
|
|
|
ON CONFLICT (guild_id, user_id) DO NOTHING`,
|
|
|
|
|
int64(guildID), int64(userID), nullableString(nickname), s.Now())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return s.GetGuildMember(ctx, guildID, userID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) GetGuildMember(ctx context.Context, guildID, userID uint64) (*GuildMember, error) {
|
|
|
|
|
var (
|
|
|
|
|
member GuildMember
|
|
|
|
|
nickname sql.NullString
|
|
|
|
|
joinedAt string
|
|
|
|
|
timeoutUntil sql.NullString
|
|
|
|
|
)
|
|
|
|
|
err := s.reader.QueryRowContext(ctx, `
|
|
|
|
|
SELECT guild_id, user_id, nickname, joined_at, timeout_until
|
|
|
|
|
FROM guild_members WHERE guild_id = ? AND user_id = ?`, int64(guildID), int64(userID)).
|
|
|
|
|
Scan(&member.GuildID, &member.UserID, &nickname, &joinedAt, &timeoutUntil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, mapError(err)
|
|
|
|
|
}
|
|
|
|
|
if nickname.Valid {
|
|
|
|
|
member.Nickname = nickname.String
|
|
|
|
|
}
|
|
|
|
|
member.JoinedAt = parseTimestamp(joinedAt)
|
|
|
|
|
if timeoutUntil.Valid {
|
|
|
|
|
value := parseTimestamp(timeoutUntil.String)
|
|
|
|
|
member.TimeoutUntil = &value
|
|
|
|
|
}
|
|
|
|
|
return &member, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) ListGuildMembers(ctx context.Context, guildID uint64) ([]GuildMember, error) {
|
|
|
|
|
rows, err := s.reader.QueryContext(ctx, `
|
|
|
|
|
SELECT guild_id, user_id, nickname, joined_at, timeout_until
|
|
|
|
|
FROM guild_members WHERE guild_id = ? ORDER BY joined_at`, int64(guildID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
members := make([]GuildMember, 0, 16)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var (
|
|
|
|
|
member GuildMember
|
|
|
|
|
nickname sql.NullString
|
|
|
|
|
joinedAt string
|
|
|
|
|
timeoutUntil sql.NullString
|
|
|
|
|
)
|
|
|
|
|
if err := rows.Scan(&member.GuildID, &member.UserID, &nickname, &joinedAt, &timeoutUntil); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if nickname.Valid {
|
|
|
|
|
member.Nickname = nickname.String
|
|
|
|
|
}
|
|
|
|
|
member.JoinedAt = parseTimestamp(joinedAt)
|
|
|
|
|
if timeoutUntil.Valid {
|
|
|
|
|
value := parseTimestamp(timeoutUntil.String)
|
|
|
|
|
member.TimeoutUntil = &value
|
|
|
|
|
}
|
|
|
|
|
members = append(members, member)
|
|
|
|
|
}
|
|
|
|
|
return members, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) UpdateGuildMember(ctx context.Context, guildID, userID uint64, nickname *string, timeoutUntil *time.Time) error {
|
|
|
|
|
sets := []string{}
|
|
|
|
|
args := []any{}
|
|
|
|
|
if nickname != nil {
|
|
|
|
|
sets = append(sets, "nickname = ?")
|
|
|
|
|
args = append(args, nullableString(*nickname))
|
|
|
|
|
}
|
|
|
|
|
if timeoutUntil != nil {
|
|
|
|
|
if timeoutUntil.IsZero() {
|
|
|
|
|
sets = append(sets, "timeout_until = NULL")
|
|
|
|
|
} else {
|
|
|
|
|
sets = append(sets, "timeout_until = ?")
|
|
|
|
|
args = append(args, s.Timestamp(*timeoutUntil))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(sets) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
args = append(args, int64(guildID), int64(userID))
|
|
|
|
|
_, err := s.writer.ExecContext(ctx, buildQuery(updateGuildMemberTemplate, joinSets(sets)), args...)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TimeoutFromNow возвращает момент окончания тайм-аута (AGENT.md 7.10).
|
|
|
|
|
func TimeoutFromNow(days int) time.Time {
|
|
|
|
|
if days < 1 {
|
|
|
|
|
days = 1
|
|
|
|
|
}
|
|
|
|
|
return time.Now().UTC().AddDate(0, 0, days)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) RemoveGuildMember(ctx context.Context, guildID, userID uint64) error {
|
|
|
|
|
_, err := s.writer.ExecContext(ctx, `DELETE FROM guild_members WHERE guild_id = ? AND user_id = ?`,
|
|
|
|
|
int64(guildID), int64(userID))
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Store) TransferGuildOwnership(ctx context.Context, guildID, newOwnerID uint64) error {
|
|
|
|
|
result, err := s.writer.ExecContext(ctx, `UPDATE guilds SET owner_id = ? WHERE id = ?`,
|
|
|
|
|
int64(newOwnerID), int64(guildID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if affected, err := result.RowsAffected(); err == nil && affected == 0 {
|
|
|
|
|
return ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scanGuild(scanner interface{ Scan(...any) error }) (*Guild, error) {
|
|
|
|
|
var (
|
|
|
|
|
guild Guild
|
|
|
|
|
iconID sql.NullInt64
|
|
|
|
|
bannerID sql.NullInt64
|
|
|
|
|
splashID sql.NullInt64
|
|
|
|
|
isMain int
|
|
|
|
|
discover int
|
|
|
|
|
limitsJSON string
|
|
|
|
|
createdAt string
|
|
|
|
|
)
|
|
|
|
|
err := scanner.Scan(&guild.ID, &guild.Name, &guild.OwnerID, &iconID, &bannerID, &splashID,
|
|
|
|
|
&guild.AccentColor, &guild.Description, &isMain, &discover, &limitsJSON, &createdAt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, mapError(err)
|
|
|
|
|
}
|
|
|
|
|
guild.IconFileID = optionalID(iconID)
|
|
|
|
|
guild.BannerFileID = optionalID(bannerID)
|
|
|
|
|
guild.SplashFileID = optionalID(splashID)
|
|
|
|
|
guild.IsMain = isMain == 1
|
|
|
|
|
guild.IsDiscoverable = discover == 1
|
|
|
|
|
if limitsJSON != "" && limitsJSON != "{}" {
|
|
|
|
|
_ = json.Unmarshal([]byte(limitsJSON), &guild.Limits)
|
|
|
|
|
}
|
|
|
|
|
guild.CreatedAt = parseTimestamp(createdAt)
|
|
|
|
|
return &guild, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func collectGuilds(rows *sql.Rows) ([]Guild, error) {
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
guilds := make([]Guild, 0, 8)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
guild, err := scanGuild(rows)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
guilds = append(guilds, *guild)
|
|
|
|
|
}
|
|
|
|
|
return guilds, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nullableString(value string) any {
|
|
|
|
|
if value == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func joinSets(sets []string) string {
|
|
|
|
|
return strings.Join(sets, ", ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// prefixColumns добавляет префикс таблицы к списку колонок.
|
|
|
|
|
func prefixColumns(prefix, columns string) string {
|
|
|
|
|
parts := strings.Split(columns, ",")
|
|
|
|
|
for i, part := range parts {
|
|
|
|
|
parts[i] = prefix + "." + strings.TrimSpace(part)
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, ", ")
|
|
|
|
|
}
|
2026-09-19 21:50:06 +03:00
|
|
|
|
|
|
|
|
// GuildMemberProfile — участник сервера вместе с публичными полями профиля:
|
|
|
|
|
// нужен списку участников в клиенте (AGENT.md 8.2).
|
|
|
|
|
type GuildMemberProfile struct {
|
|
|
|
|
UserID uint64
|
|
|
|
|
Username string
|
|
|
|
|
DisplayName string
|
|
|
|
|
Nickname string
|
|
|
|
|
AvatarFileID *uint64
|
|
|
|
|
Status string
|
|
|
|
|
CustomStatus string
|
|
|
|
|
IsInstanceAdmin bool
|
|
|
|
|
JoinedAt time.Time
|
|
|
|
|
TimeoutUntil *time.Time
|
|
|
|
|
RoleIDs []uint64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListGuildMemberProfiles возвращает участников сервера с профилями и ролями.
|
|
|
|
|
func (s *Store) ListGuildMemberProfiles(ctx context.Context, guildID uint64) ([]GuildMemberProfile, error) {
|
|
|
|
|
rows, err := s.reader.QueryContext(ctx, `
|
|
|
|
|
SELECT m.user_id, u.username, u.display_name, m.nickname, u.avatar_file_id,
|
|
|
|
|
u.status, u.custom_status, u.is_instance_admin, m.joined_at, m.timeout_until
|
|
|
|
|
FROM guild_members m
|
|
|
|
|
JOIN users u ON u.id = m.user_id
|
|
|
|
|
WHERE m.guild_id = ? AND u.deleted_at IS NULL
|
|
|
|
|
ORDER BY m.joined_at, m.user_id`, int64(guildID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
profiles := make([]GuildMemberProfile, 0, 16)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var (
|
|
|
|
|
profile GuildMemberProfile
|
|
|
|
|
nickname sql.NullString
|
|
|
|
|
avatarID sql.NullInt64
|
|
|
|
|
isAdmin int
|
|
|
|
|
joinedAt string
|
|
|
|
|
timeoutUntil sql.NullString
|
|
|
|
|
)
|
|
|
|
|
if err := rows.Scan(&profile.UserID, &profile.Username, &profile.DisplayName, &nickname,
|
|
|
|
|
&avatarID, &profile.Status, &profile.CustomStatus, &isAdmin, &joinedAt, &timeoutUntil); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if nickname.Valid {
|
|
|
|
|
profile.Nickname = nickname.String
|
|
|
|
|
}
|
|
|
|
|
if avatarID.Valid {
|
|
|
|
|
value := uint64(avatarID.Int64)
|
|
|
|
|
profile.AvatarFileID = &value
|
|
|
|
|
}
|
|
|
|
|
profile.IsInstanceAdmin = isAdmin == 1
|
|
|
|
|
profile.JoinedAt = parseTimestamp(joinedAt)
|
|
|
|
|
if timeoutUntil.Valid {
|
|
|
|
|
value := parseTimestamp(timeoutUntil.String)
|
|
|
|
|
profile.TimeoutUntil = &value
|
|
|
|
|
}
|
|
|
|
|
profiles = append(profiles, profile)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Роли участников читаем одним запросом и раскладываем по участникам.
|
|
|
|
|
roleRows, err := s.reader.QueryContext(ctx, `
|
|
|
|
|
SELECT mr.user_id, mr.role_id
|
|
|
|
|
FROM member_roles mr
|
|
|
|
|
JOIN guild_members m ON m.guild_id = mr.guild_id AND m.user_id = mr.user_id
|
|
|
|
|
WHERE mr.guild_id = ?
|
|
|
|
|
ORDER BY mr.role_id`, int64(guildID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer roleRows.Close()
|
|
|
|
|
|
|
|
|
|
index := make(map[uint64]int, len(profiles))
|
|
|
|
|
for i, profile := range profiles {
|
|
|
|
|
index[profile.UserID] = i
|
|
|
|
|
}
|
|
|
|
|
for roleRows.Next() {
|
|
|
|
|
var userID, roleID uint64
|
|
|
|
|
if err := roleRows.Scan(&userID, &roleID); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if position, ok := index[userID]; ok {
|
|
|
|
|
profiles[position].RoleIDs = append(profiles[position].RoleIDs, roleID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return profiles, roleRows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CountGuildMemberships возвращает число серверов, которыми владеет пользователь
|
|
|
|
|
// и в которых состоит: используется для лимитов (AGENT.md 6.5).
|
|
|
|
|
func (s *Store) CountGuildMemberships(ctx context.Context, userID uint64) (int, error) {
|
|
|
|
|
var count int
|
|
|
|
|
err := s.reader.QueryRowContext(ctx,
|
|
|
|
|
`SELECT COUNT(*) FROM guild_members WHERE user_id = ?`, int64(userID)).Scan(&count)
|
|
|
|
|
return count, err
|
|
|
|
|
}
|