Add Discord event announcement functionality to backend
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit introduces a new Discord event announcer to the backend, allowing for event announcements via Discord. It includes the addition of new environment variables for Discord configuration in `.env.example` and `compose.yaml`. The `main.go` file has been updated to initialize the announcer, and a new `discord` package has been created, containing the announcer logic and tests. Additionally, the service layer has been modified to support bracket draft management, enhancing the overall event workflow. Integration tests have been updated to ensure proper functionality of the new features.
This commit is contained in:
2026-07-19 11:21:11 +03:00
parent b7a78b4384
commit ae19c03542
31 changed files with 1632 additions and 88 deletions

View File

@@ -46,7 +46,7 @@ export interface MixEvent {
activeSeriesId?: string
tournamentId?: string
}
export type EventWorkflowState = 'RegistrationOpen' | 'RegistrationClosed' | 'Balancing' | 'RostersDraft' | 'RostersConfirmed' | 'Live' | 'Completed' | 'Cancelled'
export type EventWorkflowState = 'RegistrationOpen' | 'RegistrationClosed' | 'Balancing' | 'RostersDraft' | 'RostersConfirmed' | 'BracketDraft' | 'Live' | 'Completed' | 'Cancelled'
export interface TeamPlayer { player: Player; assignedRole: PlayerRole }
export interface Team {
id: string
@@ -122,6 +122,27 @@ export interface EventInput {
endsAt: string
registrationDeadline: string
}
export type BracketSlotSource =
| { kind: 'Team'; teamId: string; matchId?: never }
| { kind: 'Winner' | 'Loser'; matchId: string; teamId?: never }
export interface BracketDraftMatch {
id: string
round: number
order: number
slotA: BracketSlotSource
slotB: BracketSlotSource
seriesId?: string
teamAId?: string
teamBId?: string
winnerTeamId?: string
}
export interface BracketDraft {
eventId: string
teamIds: string[]
matches: BracketDraftMatch[]
version: number
confirmed: boolean
}
// Raw backend DTOs mirror Go's current camelCase JSON tags exactly.
export interface RawAccount {
@@ -174,7 +195,7 @@ export interface RawSeries {
export interface RawDraftAction { teamId: string; value: string; actorAccountId: string; at: string }
export interface RawTournament {
id: string; eventId: string; name: string; winnerTeamId: string
teamIds: string[]; rounds: RawSeries[][]
teamIds: string[]; rounds: RawSeries[][]; matches?: Array<BracketDraftMatch & { series?: RawSeries }>; version?: number
}
export interface RawRealtimeEvent { topic: string; data: unknown }
@@ -391,6 +412,37 @@ export function adaptSeries(raw: RawSeries, teams: Team[] = []): Series {
}
export function adaptTournament(raw: RawTournament, teams: Team[] = []): Bracket {
const names = new Map(teams.map((team) => [team.id, team.name]))
if (raw.matches?.length) {
const maxRound = Math.max(...raw.matches.map((match) => match.round))
const sourceName = (source: BracketSlotSource) => {
if (source.kind === 'Team') return names.get(source.teamId) ?? source.teamId
const upstream = raw.matches?.find((match) => match.id === source.matchId)
return `${source.kind} of ${upstream ? `R${upstream.round + 1}M${upstream.order + 1}` : 'match'}`
}
return {
id: raw.id,
title: raw.name,
rounds: Array.from({ length: maxRound + 1 }, (_, index) => index === maxRound ? 'Grand Final' : index === maxRound - 1 ? 'Semifinals' : `Round ${index + 1}`),
matches: raw.matches.map((match) => {
const adapted = match.series ? adaptSeries(match.series, teams) : undefined
return {
id: match.id,
round: match.round,
slot: match.order,
label: `Match ${match.order + 1}`,
teamAlpha: names.get(match.teamAId ?? '') ?? match.teamAId ?? sourceName(match.slotA),
teamBeta: names.get(match.teamBId ?? '') ?? match.teamBId ?? sourceName(match.slotB),
teamAlphaId: match.teamAId,
teamBetaId: match.teamBId,
scoreAlpha: adapted?.score.alpha,
scoreBeta: adapted?.score.beta,
winner: match.winnerTeamId ? (match.winnerTeamId === match.teamAId ? 'alpha' : 'beta') : undefined,
seriesId: match.seriesId ?? match.series?.id,
status: match.winnerTeamId ? 'completed' : match.series?.phase ? 'live' : match.teamAId && match.teamBId ? 'ready' : 'pending',
}
}),
}
}
return {
id: raw.id,
title: raw.name,
@@ -599,6 +651,15 @@ export const api = {
method: 'POST',
body: JSON.stringify({ expectedVersion, expectedRosterVersion }),
}),
bracketDraft: (eventId: string) => request<BracketDraft>(`/events/${eventId}/bracket-draft`),
initializeBracket: (eventId: string, expectedVersion: number) =>
request<BracketDraft>(`/events/${eventId}/bracket-draft/initialize`, { method: 'POST', body: JSON.stringify({ expectedVersion }) }),
updateBracket: (eventId: string, matches: BracketDraftMatch[], expectedVersion: number) =>
request<BracketDraft>(`/events/${eventId}/bracket-draft`, { method: 'PUT', body: JSON.stringify({ matches, expectedVersion }) }),
resetBracket: (eventId: string, expectedVersion: number) =>
request<BracketDraft>(`/events/${eventId}/bracket-draft/reset`, { method: 'POST', body: JSON.stringify({ expectedVersion }) }),
confirmBracket: (eventId: string, expectedVersion: number) =>
request<BracketDraft>(`/events/${eventId}/bracket-draft/confirm`, { method: 'POST', body: JSON.stringify({ expectedVersion }) }),
startScrim: async (eventId: string, expectedVersion: number) => {
const result = await request<{ event: RawEvent; series?: RawSeries; tournament?: RawTournament }>(`/events/${eventId}/start`, {
method: 'POST',