···
13
+
tea "github.com/charmbracelet/bubbletea"
14
+
"github.com/charmbracelet/lipgloss"
15
+
"github.com/charmbracelet/ssh"
16
+
"github.com/charmbracelet/wish"
17
+
"github.com/charmbracelet/wish/bubbletea"
18
+
"github.com/charmbracelet/wish/logging"
19
+
"github.com/charmbracelet/wish/scp"
20
+
"github.com/go-chi/chi/v5"
21
+
"github.com/go-chi/chi/v5/middleware"
23
+
"battleship-arena/internal/runner"
24
+
"battleship-arena/internal/server"
25
+
"battleship-arena/internal/storage"
26
+
"battleship-arena/internal/tui"
29
+
type Config struct {
35
+
AdminPasscode string
39
+
func loadConfig() Config {
41
+
Host: getEnv("BATTLESHIP_HOST", "0.0.0.0"),
42
+
SSHPort: getEnv("BATTLESHIP_SSH_PORT", "2222"),
43
+
WebPort: getEnv("BATTLESHIP_WEB_PORT", "8081"),
44
+
UploadDir: getEnv("BATTLESHIP_UPLOAD_DIR", "./submissions"),
45
+
ResultsDB: getEnv("BATTLESHIP_RESULTS_DB", "./results.db"),
46
+
AdminPasscode: getEnv("BATTLESHIP_ADMIN_PASSCODE", "battleship-admin-override"),
47
+
ExternalURL: getEnv("BATTLESHIP_EXTERNAL_URL", "http://localhost:8081"),
52
+
func getEnv(key, defaultValue string) string {
53
+
if value := os.Getenv(key); value != "" {
62
+
if err := initStorage(cfg); err != nil {
67
+
server.SetConfig(cfg.AdminPasscode, cfg.ExternalURL)
69
+
workerCtx, workerCancel := context.WithCancel(context.Background())
70
+
defer workerCancel()
71
+
go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete)
73
+
toClient, fromClient := server.NewSCPHandlers(cfg.UploadDir)
74
+
sshServer, err := wish.NewServer(
75
+
wish.WithAddress(cfg.Host + ":" + cfg.SSHPort),
76
+
wish.WithHostKeyPath(".ssh/battleship_arena"),
77
+
wish.WithPublicKeyAuth(server.PublicKeyAuthHandler),
78
+
wish.WithPasswordAuth(server.PasswordAuthHandler),
79
+
wish.WithSubsystem("sftp", server.SFTPHandler(cfg.UploadDir)),
80
+
wish.WithMiddleware(
81
+
scp.Middleware(toClient, fromClient),
82
+
bubbletea.Middleware(teaHandler),
83
+
logging.Middleware(),
90
+
done := make(chan os.Signal, 1)
91
+
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
93
+
log.Printf("SSH server listening on %s:%s", cfg.Host, cfg.SSHPort)
94
+
log.Printf("Web leaderboard at %s", cfg.ExternalURL)
97
+
if err := sshServer.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
104
+
log.Println("Shutting down servers...")
105
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
107
+
if err := sshServer.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
113
+
r := chi.NewRouter()
114
+
r.Use(middleware.Logger)
115
+
r.Use(middleware.Recoverer)
116
+
r.Mount("/events/", server.SSEServer)
117
+
r.Get("/api/leaderboard", server.HandleAPILeaderboard)
118
+
r.Get("/api/rating-history/{player}", server.HandleRatingHistory)
119
+
r.Get("/player/{player}", server.HandlePlayerPage)
120
+
r.Get("/user/{username}", server.HandleUserProfile)
121
+
r.Get("/users", server.HandleUsers)
122
+
r.Get("/", server.HandleLeaderboard)
124
+
log.Println("Server running at " + cfg.ExternalURL)
125
+
http.ListenAndServe(":"+cfg.WebPort, r)
128
+
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
129
+
if len(s.Command()) > 0 {
133
+
// Check if user needs onboarding
134
+
needsOnboarding := false
135
+
if val := s.Context().Value("needs_onboarding"); val != nil {
136
+
needsOnboarding = val.(bool)
139
+
pty, _, active := s.Pty()
141
+
wish.Fatalln(s, "no active terminal")
145
+
if needsOnboarding {
146
+
// Run onboarding first
148
+
if val := s.Context().Value("public_key"); val != nil {
149
+
publicKey = val.(string)
152
+
m := tui.NewOnboardingModel(s.User(), publicKey, pty.Window.Width, pty.Window.Height)
153
+
return m, []tea.ProgramOption{tea.WithAltScreen()}
156
+
m := tui.InitialModel(s.User(), pty.Window.Width, pty.Window.Height)
157
+
return m, []tea.ProgramOption{tea.WithAltScreen()}
160
+
func initStorage(cfg Config) error {
161
+
if err := os.MkdirAll(cfg.UploadDir, 0755); err != nil {
165
+
db, err := storage.InitDB(cfg.ResultsDB)
174
+
var titleStyle = lipgloss.NewStyle().
176
+
Foreground(lipgloss.Color("205")).