Add Discord event announcement functionality to backend
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:
@@ -393,6 +393,132 @@ func (s *Service) ConfirmRosters(ctx context.Context, actor domain.Account, even
|
||||
return event, err
|
||||
}
|
||||
|
||||
func (s *Service) InitializeBracket(ctx context.Context, actor domain.Account, eventID string, expectedEventVersion int) (domain.BracketDraft, error) {
|
||||
if !actor.IsStaff() {
|
||||
return domain.BracketDraft{}, domain.ErrForbidden
|
||||
}
|
||||
event, err := s.Store.GetEvent(ctx, eventID)
|
||||
if err != nil {
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
roster, err := s.Store.GetRoster(ctx, eventID)
|
||||
if err != nil {
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
if !roster.Confirmed {
|
||||
return domain.BracketDraft{}, fmt.Errorf("%w: rosters are not confirmed", domain.ErrConflict)
|
||||
}
|
||||
teamIDs := make([]string, len(roster.Teams))
|
||||
for index := range roster.Teams {
|
||||
teamIDs[index] = roster.Teams[index].ID
|
||||
}
|
||||
draft, err := domain.NewBracketDraft(eventID, teamIDs)
|
||||
if err != nil {
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
old := event.Version
|
||||
if err = event.Transition([]domain.EventState{domain.RostersConfirmed}, domain.BracketDraftState, expectedEventVersion); err != nil {
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
if _, err = s.Store.SaveBracketDraft(ctx, *draft, -1); err != nil {
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
event.UpdatedAt = s.Now()
|
||||
if _, err = s.Store.SaveEventWorkflow(ctx, event, old); err == nil {
|
||||
s.Bus.Publish("event:"+eventID, event)
|
||||
s.Bus.Publish("bracket:"+eventID, draft)
|
||||
}
|
||||
return *draft, err
|
||||
}
|
||||
|
||||
func (s *Service) ResetBracket(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.BracketDraft, error) {
|
||||
if !actor.IsStaff() {
|
||||
return domain.BracketDraft{}, domain.ErrForbidden
|
||||
}
|
||||
event, err := s.Store.GetEvent(ctx, eventID)
|
||||
if err != nil || event.State != domain.BracketDraftState {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("%w: bracket is not editable", domain.ErrConflict)
|
||||
}
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
current, err := s.Store.GetBracketDraft(ctx, eventID)
|
||||
if err != nil || current.Version != expectedVersion {
|
||||
if err == nil {
|
||||
err = domain.ErrConflict
|
||||
}
|
||||
return current, err
|
||||
}
|
||||
draft, err := domain.NewBracketDraft(eventID, current.TeamIDs)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
draft.Version = current.Version + 1
|
||||
out, err := s.Store.SaveBracketDraft(ctx, *draft, current.Version)
|
||||
if err == nil {
|
||||
s.Bus.Publish("bracket:"+eventID, out)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Service) UpdateBracket(ctx context.Context, actor domain.Account, eventID string, matches []domain.BracketMatch, expectedVersion int) (domain.BracketDraft, error) {
|
||||
if !actor.IsStaff() {
|
||||
return domain.BracketDraft{}, domain.ErrForbidden
|
||||
}
|
||||
event, err := s.Store.GetEvent(ctx, eventID)
|
||||
if err != nil || event.State != domain.BracketDraftState {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("%w: bracket is not editable", domain.ErrConflict)
|
||||
}
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
draft, err := s.Store.GetBracketDraft(ctx, eventID)
|
||||
if err != nil || draft.Version != expectedVersion {
|
||||
if err == nil {
|
||||
err = domain.ErrConflict
|
||||
}
|
||||
return draft, err
|
||||
}
|
||||
draft.Matches, draft.Confirmed, draft.Version = matches, false, draft.Version+1
|
||||
if err = draft.Validate(); err != nil {
|
||||
return draft, err
|
||||
}
|
||||
out, err := s.Store.SaveBracketDraft(ctx, draft, expectedVersion)
|
||||
if err == nil {
|
||||
s.Bus.Publish("bracket:"+eventID, out)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Service) ConfirmBracket(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.BracketDraft, error) {
|
||||
if !actor.IsStaff() {
|
||||
return domain.BracketDraft{}, domain.ErrForbidden
|
||||
}
|
||||
event, err := s.Store.GetEvent(ctx, eventID)
|
||||
if err != nil || event.State != domain.BracketDraftState {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("%w: bracket is not editable", domain.ErrConflict)
|
||||
}
|
||||
return domain.BracketDraft{}, err
|
||||
}
|
||||
draft, err := s.Store.GetBracketDraft(ctx, eventID)
|
||||
if err != nil || draft.Version != expectedVersion {
|
||||
if err == nil {
|
||||
err = domain.ErrConflict
|
||||
}
|
||||
return draft, err
|
||||
}
|
||||
if err = draft.Validate(); err != nil {
|
||||
return draft, err
|
||||
}
|
||||
draft.Confirmed, draft.Version = true, draft.Version+1
|
||||
out, err := s.Store.SaveBracketDraft(ctx, draft, expectedVersion)
|
||||
if err == nil {
|
||||
s.Bus.Publish("bracket:"+eventID, out)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Service) RevertWorkflowStage(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.Event, error) {
|
||||
if !actor.IsStaff() {
|
||||
return domain.Event{}, domain.ErrForbidden
|
||||
@@ -412,6 +538,8 @@ func (s *Service) RevertWorkflowStage(ctx context.Context, actor domain.Account,
|
||||
previous = domain.Balancing
|
||||
case domain.RostersConfirmed:
|
||||
previous = domain.RostersDraft
|
||||
case domain.BracketDraftState:
|
||||
previous = domain.RostersConfirmed
|
||||
default:
|
||||
return event, fmt.Errorf("%w: this workflow stage cannot be reverted", domain.ErrConflict)
|
||||
}
|
||||
@@ -435,6 +563,9 @@ func (s *Service) RevertWorkflowStage(ctx context.Context, actor domain.Account,
|
||||
return event, rosterErr
|
||||
}
|
||||
}
|
||||
if event.State == domain.RostersConfirmed && previous == domain.RostersConfirmed {
|
||||
_ = s.Store.DeleteBracketDraft(ctx, eventID)
|
||||
}
|
||||
event.UpdatedAt = s.Now()
|
||||
event, err = s.Store.SaveEventWorkflow(ctx, event, oldVersion)
|
||||
if err == nil {
|
||||
@@ -459,52 +590,50 @@ func (s *Service) StartScrim(ctx context.Context, actor domain.Account, eventID
|
||||
if !roster.Confirmed {
|
||||
return ScrimStart{}, fmt.Errorf("%w: rosters are not confirmed", domain.ErrConflict)
|
||||
}
|
||||
draft, err := s.Store.GetBracketDraft(ctx, eventID)
|
||||
if err != nil {
|
||||
return ScrimStart{}, err
|
||||
}
|
||||
if !draft.Confirmed {
|
||||
return ScrimStart{}, fmt.Errorf("%w: bracket is not confirmed", domain.ErrConflict)
|
||||
}
|
||||
if err = draft.Validate(); err != nil {
|
||||
return ScrimStart{}, err
|
||||
}
|
||||
rules, err := s.Store.GetRuleset(ctx, event.RulesetID)
|
||||
if err != nil {
|
||||
return ScrimStart{}, err
|
||||
}
|
||||
oldVersion := event.Version
|
||||
if err = event.Transition([]domain.EventState{domain.RostersConfirmed}, domain.Live, expectedVersion); err != nil {
|
||||
if err = event.Transition([]domain.EventState{domain.BracketDraftState}, domain.Live, expectedVersion); err != nil {
|
||||
return ScrimStart{}, err
|
||||
}
|
||||
result := ScrimStart{}
|
||||
seriesToSave := make([]domain.Series, 0)
|
||||
teamIDs := make([]string, len(roster.Teams))
|
||||
for i := range roster.Teams {
|
||||
teamIDs[i] = roster.Teams[i].ID
|
||||
tournament, err := domain.NewGraphTournament(NewID(), eventID, event.Name, draft)
|
||||
if err != nil {
|
||||
return ScrimStart{}, err
|
||||
}
|
||||
if len(teamIDs) == 2 {
|
||||
series, createErr := domain.NewSeries(NewID(), eventID, "", [2]string{teamIDs[0], teamIDs[1]}, rules)
|
||||
for _, ready := range tournament.ReadyMatches() {
|
||||
series, createErr := domain.NewSeries(ready.ID, eventID, tournament.ID, [2]string{ready.TeamAID, ready.TeamBID}, rules)
|
||||
if createErr != nil {
|
||||
return ScrimStart{}, createErr
|
||||
}
|
||||
seriesToSave = append(seriesToSave, *series)
|
||||
event.ActiveSeriesID = series.ID
|
||||
result.Series = series
|
||||
} else if len(teamIDs) >= 4 {
|
||||
tournament, createErr := domain.NewTournament(NewID(), eventID, event.Name, teamIDs)
|
||||
if createErr != nil {
|
||||
return ScrimStart{}, createErr
|
||||
}
|
||||
for round := range tournament.Rounds {
|
||||
for match := range tournament.Rounds[round] {
|
||||
base := tournament.Rounds[round][match]
|
||||
if base.TeamAID == "" || base.TeamBID == "" {
|
||||
continue
|
||||
}
|
||||
series, seriesErr := domain.NewSeries(base.ID, eventID, tournament.ID, [2]string{base.TeamAID, base.TeamBID}, rules)
|
||||
if seriesErr != nil {
|
||||
return ScrimStart{}, seriesErr
|
||||
}
|
||||
tournament.Rounds[round][match] = *series
|
||||
seriesToSave = append(seriesToSave, *series)
|
||||
for index := range tournament.Matches {
|
||||
if tournament.Matches[index].ID == ready.ID {
|
||||
tournament.Matches[index].SeriesID = series.ID
|
||||
tournament.Matches[index].Series = series
|
||||
}
|
||||
}
|
||||
event.TournamentID = tournament.ID
|
||||
result.Tournament = tournament
|
||||
} else {
|
||||
return ScrimStart{}, fmt.Errorf("%w: scrim needs two or at least four teams", domain.ErrInvalid)
|
||||
seriesToSave = append(seriesToSave, *series)
|
||||
if result.Series == nil {
|
||||
result.Series = series
|
||||
event.ActiveSeriesID = series.ID
|
||||
}
|
||||
}
|
||||
tournament.BuildRounds()
|
||||
event.TournamentID = tournament.ID
|
||||
result.Tournament = tournament
|
||||
event.UpdatedAt = s.Now()
|
||||
err = s.Store.StartScrim(ctx, event, oldVersion, seriesToSave, result.Tournament)
|
||||
if err == nil {
|
||||
@@ -562,6 +691,9 @@ func (s *Service) RecordSeriesResult(ctx context.Context, actor domain.Account,
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if len(tournament.Matches) > 0 {
|
||||
return out, s.advanceGraphTournament(ctx, out, tournament)
|
||||
}
|
||||
for round := range tournament.Rounds {
|
||||
for match := range tournament.Rounds[round] {
|
||||
if tournament.Rounds[round][match].ID != out.ID {
|
||||
@@ -610,6 +742,67 @@ func (s *Service) RecordSeriesResult(ctx context.Context, actor domain.Account,
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Service) advanceGraphTournament(ctx context.Context, completed domain.Series, tournament domain.Tournament) error {
|
||||
event, err := s.Store.GetEvent(ctx, completed.EventID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := s.Store.GetRuleset(ctx, event.RulesetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if err = tournament.ApplySeries(completed); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ready := range tournament.ReadyMatches() {
|
||||
series, getErr := s.Store.GetSeries(ctx, ready.ID)
|
||||
if getErr == domain.ErrNotFound {
|
||||
created, createErr := domain.NewSeries(ready.ID, completed.EventID, tournament.ID, [2]string{ready.TeamAID, ready.TeamBID}, rules)
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
series = *created
|
||||
if _, createErr = s.Store.SaveSeries(ctx, series); createErr != nil && createErr != domain.ErrConflict {
|
||||
return createErr
|
||||
}
|
||||
} else if getErr != nil {
|
||||
return getErr
|
||||
}
|
||||
for index := range tournament.Matches {
|
||||
if tournament.Matches[index].ID == ready.ID {
|
||||
tournament.Matches[index].SeriesID = series.ID
|
||||
copy := series
|
||||
tournament.Matches[index].Series = ©
|
||||
}
|
||||
}
|
||||
}
|
||||
tournament.BuildRounds()
|
||||
if _, err = s.Store.SaveTournament(ctx, tournament); err != domain.ErrConflict {
|
||||
break
|
||||
}
|
||||
tournament, err = s.Store.GetTournament(ctx, tournament.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Bus.Publish("tournament:"+tournament.ID, tournament)
|
||||
s.Bus.Publish("event:"+completed.EventID, tournament)
|
||||
if tournament.Complete() {
|
||||
event, err = s.Store.GetEvent(ctx, completed.EventID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
old := event.Version
|
||||
event.State, event.Version, event.UpdatedAt = domain.Completed, event.Version+1, s.Now()
|
||||
_, err = s.Store.SaveEventWorkflow(ctx, event, old)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) mutateSeries(ctx context.Context, actor domain.Account, player domain.Player, seriesID, actingTeamID string, expectedVersion int, action string, mutation func(*domain.Series, domain.Ruleset) error) (domain.Series, error) {
|
||||
series, err := s.Store.GetSeries(ctx, seriesID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user