Files
mixmaker/backend/internal/adapter/postgres/discord_roles.go
lemintare e5646ba33d
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled
Enhance Discord role management by adding hoist functionality
This commit introduces a new `hoist` attribute for Discord roles, allowing team roles and the registered role to be displayed in separate groups within the Discord member list. The `RoleWorker` has been updated to ensure team roles are positioned above the registered role, improving visibility and organization. Database schema changes have been made to support the new `hoist` field, and corresponding updates have been implemented in the service layer and tests to validate the new behavior.
2026-07-19 11:52:57 +03:00

249 lines
8.7 KiB
Go

package postgres
import (
"context"
"encoding/json"
"time"
"mixmaker/backend/internal/application"
"mixmaker/backend/internal/domain"
)
func (s *Store) EnqueueDiscordRoleSync(ctx context.Context, eventID, action string, generation int64) error {
_, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation,role_snapshot)
SELECT $1,$2,$3,COALESCE(jsonb_agg(discord_role_id) FILTER (WHERE discord_role_id IS NOT NULL),'[]'::jsonb)
FROM discord_managed_roles WHERE scope='event' AND event_id=$1
ON CONFLICT(event_id,action,generation) DO UPDATE
SET status='pending',available_at=now(),updated_at=now(),completed_at=NULL`,
eventID, action, generation)
return err
}
func (s *Store) SeedDiscordRoleSyncJobs(ctx context.Context) error {
if _, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation)
SELECT e.id,'reconcile',e.version
FROM events e
JOIN event_rosters er ON er.event_id=e.id
WHERE e.state IN ('RostersConfirmed','BracketDraft','Live')
AND COALESCE((er.body->>'confirmed')::boolean,false)=true
ON CONFLICT(event_id,action,generation) DO NOTHING`); err != nil {
return err
}
_, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation)
SELECT DISTINCT e.id,'rsvp',e.version
FROM events e JOIN rsvps r ON r.event_id=e.id
WHERE e.state NOT IN ('Completed','Cancelled')
ON CONFLICT(event_id,action,generation) DO NOTHING`)
return err
}
func (s *Store) ClaimDiscordRoleSyncJob(ctx context.Context) (application.DiscordRoleSyncJob, error) {
var job application.DiscordRoleSyncJob
var snapshot []byte
err := s.pool.QueryRow(ctx, `WITH candidate AS (
SELECT id FROM discord_role_sync_jobs
WHERE (status='pending' AND available_at<=now())
OR (status='processing' AND updated_at<now()-interval '5 minutes')
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE discord_role_sync_jobs j
SET status='processing',attempts=j.attempts+1,updated_at=now()
FROM candidate
WHERE j.id=candidate.id
RETURNING j.id,j.event_id,j.action,j.generation,j.role_snapshot,j.attempts`).
Scan(&job.ID, &job.EventID, &job.Action, &job.Generation, &snapshot, &job.Attempts)
if err != nil {
return job, mapError(err)
}
if err = json.Unmarshal(snapshot, &job.RoleSnapshot); err != nil {
return job, err
}
return job, nil
}
func (s *Store) CompleteDiscordRoleSyncJob(ctx context.Context, jobID int64, warning string) error {
_, err := s.pool.Exec(ctx, `UPDATE discord_role_sync_jobs
SET status='completed',last_error=$2,completed_at=now(),updated_at=now()
WHERE id=$1`, jobID, warning)
return err
}
func (s *Store) RetryDiscordRoleSyncJob(ctx context.Context, jobID int64, message string, availableAt time.Time) error {
_, err := s.pool.Exec(ctx, `UPDATE discord_role_sync_jobs
SET status='pending',last_error=$2,available_at=$3,updated_at=now()
WHERE id=$1`, jobID, message, availableAt)
return err
}
func (s *Store) ListDiscordManagedRoles(ctx context.Context, eventID string) ([]application.DiscordManagedRole, error) {
rows, err := s.pool.Query(ctx, `SELECT scope,event_id,team_id,kind,discord_role_id,role_name,hoist
FROM discord_managed_roles
WHERE scope='global' OR event_id=$1
ORDER BY scope,event_id,team_id,kind`, eventID)
if err != nil {
return nil, err
}
defer rows.Close()
roles := make([]application.DiscordManagedRole, 0)
for rows.Next() {
var role application.DiscordManagedRole
if err = rows.Scan(&role.Scope, &role.EventID, &role.TeamID, &role.Kind, &role.DiscordRoleID, &role.RoleName, &role.Hoist); err != nil {
return nil, err
}
roles = append(roles, role)
}
return roles, rows.Err()
}
func (s *Store) UpsertDiscordManagedRole(ctx context.Context, role application.DiscordManagedRole) error {
_, err := s.pool.Exec(ctx, `INSERT INTO discord_managed_roles(scope,event_id,team_id,kind,discord_role_id,role_name,hoist)
VALUES($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT(scope,event_id,team_id,kind) DO UPDATE
SET discord_role_id=excluded.discord_role_id,role_name=excluded.role_name,hoist=excluded.hoist,updated_at=now()`,
role.Scope, role.EventID, role.TeamID, role.Kind, role.DiscordRoleID, role.RoleName, role.Hoist)
return err
}
func (s *Store) DeleteDiscordManagedRole(ctx context.Context, role application.DiscordManagedRole) error {
_, err := s.pool.Exec(ctx, `DELETE FROM discord_managed_roles
WHERE scope=$1 AND event_id=$2 AND team_id=$3 AND kind=$4`,
role.Scope, role.EventID, role.TeamID, role.Kind)
return err
}
func (s *Store) ListDiscordRoleAssignments(ctx context.Context, discordRoleID string) ([]string, error) {
rows, err := s.pool.Query(ctx, `SELECT discord_user_id FROM discord_role_assignments
WHERE discord_role_id=$1 ORDER BY discord_user_id`, discordRoleID)
if err != nil {
return nil, err
}
defer rows.Close()
userIDs := make([]string, 0)
for rows.Next() {
var userID string
if err = rows.Scan(&userID); err != nil {
return nil, err
}
userIDs = append(userIDs, userID)
}
return userIDs, rows.Err()
}
func (s *Store) UpsertDiscordRoleAssignment(ctx context.Context, discordRoleID, discordUserID string) error {
_, err := s.pool.Exec(ctx, `INSERT INTO discord_role_assignments(discord_role_id,discord_user_id)
VALUES($1,$2) ON CONFLICT(discord_role_id,discord_user_id) DO UPDATE SET updated_at=now()`,
discordRoleID, discordUserID)
return err
}
func (s *Store) DeleteDiscordRoleAssignment(ctx context.Context, discordRoleID, discordUserID string) error {
_, err := s.pool.Exec(ctx, `DELETE FROM discord_role_assignments
WHERE discord_role_id=$1 AND discord_user_id=$2`, discordRoleID, discordUserID)
return err
}
func (s *Store) GetDiscordRoleRoster(ctx context.Context, eventID string) (application.DiscordRoleRoster, error) {
roster, err := s.GetRoster(ctx, eventID)
if err != nil {
return application.DiscordRoleRoster{}, err
}
return s.discordRoleRoster(ctx, roster)
}
func (s *Store) ListActiveDiscordRoleRosters(ctx context.Context) ([]application.DiscordRoleRoster, error) {
rows, err := s.pool.Query(ctx, `SELECT er.body
FROM event_rosters er
JOIN events e ON e.id=er.event_id
WHERE e.state IN ('RostersConfirmed','BracketDraft','Live')
AND COALESCE((er.body->>'confirmed')::boolean,false)=true
ORDER BY er.event_id`)
if err != nil {
return nil, err
}
defer rows.Close()
rosters := make([]domain.RosterDraft, 0)
for rows.Next() {
var body []byte
var roster domain.RosterDraft
if err = rows.Scan(&body); err != nil {
return nil, err
}
if err = json.Unmarshal(body, &roster); err != nil {
return nil, err
}
rosters = append(rosters, roster)
}
if err = rows.Err(); err != nil {
return nil, err
}
out := make([]application.DiscordRoleRoster, 0, len(rosters))
for _, roster := range rosters {
item, rosterErr := s.discordRoleRoster(ctx, roster)
if rosterErr != nil {
return nil, rosterErr
}
out = append(out, item)
}
return out, nil
}
func (s *Store) GetDiscordRoleRegistration(ctx context.Context, eventID string) (application.DiscordRoleRegistration, error) {
event, err := s.GetEvent(ctx, eventID)
if err != nil {
return application.DiscordRoleRegistration{}, err
}
registrations, err := s.ListRSVPs(ctx, eventID)
if err != nil {
return application.DiscordRoleRegistration{}, err
}
playerIDs := make([]string, 0, len(registrations))
for _, registration := range registrations {
playerIDs = append(playerIDs, registration.PlayerID)
}
discordIDs, err := s.playerDiscordIDs(ctx, playerIDs)
if err != nil {
return application.DiscordRoleRegistration{}, err
}
return application.DiscordRoleRegistration{Event: event, Registrations: registrations, PlayerDiscordIDs: discordIDs}, nil
}
func (s *Store) discordRoleRoster(ctx context.Context, roster domain.RosterDraft) (application.DiscordRoleRoster, error) {
playerIDs := make([]string, 0)
for _, team := range roster.Teams {
for _, slot := range team.Slots {
if slot.PlayerID != "" {
playerIDs = append(playerIDs, slot.PlayerID)
}
}
}
discordIDs, err := s.playerDiscordIDs(ctx, playerIDs)
if err != nil {
return application.DiscordRoleRoster{}, err
}
return application.DiscordRoleRoster{Roster: roster, PlayerDiscordIDs: discordIDs}, nil
}
func (s *Store) playerDiscordIDs(ctx context.Context, playerIDs []string) (map[string]string, error) {
discordIDs := make(map[string]string)
if len(playerIDs) == 0 {
return discordIDs, nil
}
rows, err := s.pool.Query(ctx, `SELECT p.id,a.discord_id
FROM players p JOIN accounts a ON a.id=p.account_id
WHERE p.id=ANY($1)`, playerIDs)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var playerID, discordID string
if err = rows.Scan(&playerID, &discordID); err != nil {
return nil, err
}
discordIDs[playerID] = discordID
}
return discordIDs, rows.Err()
}