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.
128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
discordadapter "mixmaker/backend/internal/adapter/discord"
|
|
"mixmaker/backend/internal/adapter/httpapi"
|
|
"mixmaker/backend/internal/adapter/postgres"
|
|
"mixmaker/backend/internal/application"
|
|
"mixmaker/backend/internal/realtime"
|
|
)
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
databaseURL := required("DATABASE_URL")
|
|
if len(os.Args) > 1 && os.Args[1] == "migrate" {
|
|
dir := env("MIGRATIONS_DIR", "migrations")
|
|
if err := postgres.Migrate(ctx, databaseURL, dir); err != nil {
|
|
slog.Error("migration failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
slog.Info("migrations applied")
|
|
return
|
|
}
|
|
|
|
store, err := postgres.Open(ctx, databaseURL)
|
|
if err != nil {
|
|
slog.Error("database connection failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer store.Close()
|
|
hub := realtime.New()
|
|
var eventAnnouncer application.EventAnnouncer
|
|
discordBotToken := os.Getenv("DISCORD_BOT_TOKEN")
|
|
discordAnnouncementChannelID := os.Getenv("DISCORD_ANNOUNCEMENT_CHANNEL_ID")
|
|
if discordBotToken != "" || discordAnnouncementChannelID != "" {
|
|
eventAnnouncer, err = discordadapter.New(discordadapter.Config{
|
|
BotToken: discordBotToken,
|
|
ChannelID: discordAnnouncementChannelID,
|
|
PublicURL: env("PUBLIC_URL", env("FRONTEND_URL", "")),
|
|
Locale: env("DISCORD_ANNOUNCEMENT_LOCALE", "ru"),
|
|
})
|
|
if err != nil {
|
|
slog.Error("Discord announcer configuration failed", "error", err)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
service := application.New(store, hub, eventAnnouncer)
|
|
cfg := httpapi.Config{
|
|
DiscordClientID: required("DISCORD_CLIENT_ID"),
|
|
DiscordClientSecret: required("DISCORD_CLIENT_SECRET"),
|
|
DiscordRedirectURL: required("DISCORD_REDIRECT_URL"),
|
|
FrontendURL: env("FRONTEND_URL", "/"),
|
|
CookieName: env("SESSION_COOKIE_NAME", "mixmaker_session"),
|
|
SecureCookies: env("COOKIE_SECURE", "true") == "true",
|
|
SessionTTL: durationEnv("SESSION_TTL", 7*24*time.Hour),
|
|
AdminDiscordIDs: csvSet(os.Getenv("ADMIN_DISCORD_IDS")),
|
|
}
|
|
server := &http.Server{
|
|
Addr: env("HTTP_ADDR", ":8080"),
|
|
Handler: httpapi.New(service, store, hub, cfg),
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
go func() {
|
|
slog.Info("API listening", "address", server.Addr)
|
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
slog.Error("server failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
stop, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
|
defer cancel()
|
|
<-stop.Done()
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer shutdownCancel()
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
slog.Error("graceful shutdown failed", "error", err)
|
|
}
|
|
}
|
|
|
|
func required(key string) string {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
slog.Error("required environment variable missing", "key", key)
|
|
os.Exit(2)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func csvSet(value string) map[string]bool {
|
|
out := make(map[string]bool)
|
|
for _, item := range strings.Split(value, ",") {
|
|
if item = strings.TrimSpace(item); item != "" {
|
|
out[item] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func durationEnv(key string, fallback time.Duration) time.Duration {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := time.ParseDuration(value)
|
|
if err != nil || parsed <= 0 {
|
|
slog.Error("invalid duration environment variable", "key", key, "value", value)
|
|
os.Exit(2)
|
|
}
|
|
return parsed
|
|
}
|