summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--api/handlers/handlers.go62
-rw-r--r--cmd/api/main.go1
-rw-r--r--cmd/ui/main.go19
-rw-r--r--network/network.go14
-rw-r--r--ui/views/play.go189
6 files changed, 246 insertions, 40 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..35cce1c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.rahannarc
diff --git a/api/handlers/handlers.go b/api/handlers/handlers.go
index 9166ec7..fe2ca58 100644
--- a/api/handlers/handlers.go
+++ b/api/handlers/handlers.go
@@ -4,12 +4,14 @@ import (
"encoding/json"
"log/slog"
"net/http"
+ "time"
"github.com/boozec/rahanna/api/auth"
"github.com/boozec/rahanna/api/database"
utils "github.com/boozec/rahanna/pkg"
"github.com/boozec/rahanna/relay"
"golang.org/x/crypto/bcrypt"
+ "gorm.io/gorm"
)
type NewPlayRequest struct {
@@ -136,3 +138,63 @@ func NewPlay(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"name": name})
}
+
+func EnterPlay(w http.ResponseWriter, r *http.Request) {
+ slog.Info("POST /enter-play")
+ claims, err := auth.ValidateJWT(r.Header.Get("Authorization"))
+
+ if err != nil {
+ utils.JsonError(&w, err.Error())
+ return
+ }
+
+ var payload struct {
+ Name string `json:"name"`
+ IP string `json:"ip"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
+ utils.JsonError(&w, err.Error())
+ return
+ }
+
+ if err != nil {
+ utils.JsonError(&w, err.Error())
+ return
+ }
+
+ db, _ := database.GetDb()
+
+ var play database.Play
+
+ result := db.Where("name = ? AND player2_id IS NULL", payload.Name).First(&play)
+ if result.Error != nil {
+ utils.JsonError(&w, result.Error.Error())
+ return
+ }
+
+ play.Player2ID = &claims.UserID
+ play.IP2 = payload.IP
+ play.UpdatedAt = time.Now()
+
+ if err := db.Save(&play).Error; err != nil {
+ utils.JsonError(&w, err.Error())
+ return
+ }
+
+ result = db.Where("id = ?", play.ID).
+ Preload("Player1", func(db *gorm.DB) *gorm.DB {
+ return db.Omit("Password")
+ }).
+ Preload("Player2", func(db *gorm.DB) *gorm.DB {
+ return db.Omit("Password")
+ }).
+ First(&play)
+
+ if result.Error != nil {
+ utils.JsonError(&w, result.Error.Error())
+ return
+ }
+
+ json.NewEncoder(w).Encode(play)
+}
diff --git a/cmd/api/main.go b/cmd/api/main.go
index 67ebe07..b1f912f 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -19,6 +19,7 @@ func main() {
r.HandleFunc("/auth/register", handlers.RegisterUser).Methods(http.MethodPost)
r.HandleFunc("/auth/login", handlers.LoginUser).Methods(http.MethodPost)
r.Handle("/play", middleware.AuthMiddleware(http.HandlerFunc(handlers.NewPlay))).Methods(http.MethodPost)
+ r.Handle("/enter-play", middleware.AuthMiddleware(http.HandlerFunc(handlers.EnterPlay))).Methods(http.MethodPost)
slog.Info("Serving on :8080")
handler := cors.AllowAll().Handler(r)
diff --git a/cmd/ui/main.go b/cmd/ui/main.go
new file mode 100644
index 0000000..4bdcce9
--- /dev/null
+++ b/cmd/ui/main.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+ "log"
+
+ "github.com/boozec/rahanna/ui/views"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func main() {
+ views.ClearScreen()
+
+ p := tea.NewProgram(views.NewRahannaModel(), tea.WithAltScreen())
+
+ if _, err := p.Run(); err != nil {
+ log.Fatal(err)
+ }
+ views.ClearScreen()
+}
diff --git a/network/network.go b/network/network.go
index 322487b..5ab2dac 100644
--- a/network/network.go
+++ b/network/network.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
+ "math/rand"
"net"
"sync"
"time"
@@ -224,3 +225,16 @@ func GetOutboundIP() net.IP {
return localAddr.IP
}
+
+func GetRandomAvailablePort() (int, error) {
+ for i := 0; i < 100; i += 1 {
+ port := rand.Intn(65535-1024) + 1024
+ addr := fmt.Sprintf(":%d", port)
+ ln, err := net.Listen("tcp", addr)
+ if err == nil {
+ defer ln.Close()
+ return port, nil
+ }
+ }
+ return 0, fmt.Errorf("failed to find an available port after multiple attempts")
+}
diff --git a/ui/views/play.go b/ui/views/play.go
index b6afbd1..d4f6b3b 100644
--- a/ui/views/play.go
+++ b/ui/views/play.go
@@ -4,11 +4,11 @@ import (
"bufio"
"bytes"
"encoding/json"
- "errors"
"fmt"
"net/http"
"os"
+ "github.com/boozec/rahanna/api/database"
"github.com/boozec/rahanna/network"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
@@ -79,6 +79,7 @@ type PlayModel struct {
page PlayModelPage
isLoading bool
playName string
+ play *database.Play
}
func NewPlayModel(width, height int) PlayModel {
@@ -99,6 +100,7 @@ func NewPlayModel(width, height int) PlayModel {
page: LandingPage,
isLoading: false,
playName: "",
+ play: nil,
}
}
@@ -130,26 +132,34 @@ func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.newPlayCallback()
}
case key.Matches(msg, m.keys.GoLogout):
- if err := os.Remove(".rahannarc"); err != nil {
- m.err = err
- return m, nil
- }
- return m, SwitchModelCmd(NewAuthModel(m.width, m.height+1))
+ return m, m.logout()
case key.Matches(msg, m.keys.Quit):
return m, tea.Quit
case msg.Type == tea.KeyEnter:
if m.page == InsertCodePage {
- m.err = errors.New("Can't join for now...")
+ if !m.isLoading {
+ m.isLoading = true
+ return m, m.enterPlay()
+ }
}
}
case playResponse:
m.isLoading = false
+ m.err = nil
if msg.Error != "" {
m.err = fmt.Errorf(msg.Error)
+ if msg.Error == "unauthorized" {
+ return m, m.logout()
+ }
} else {
m.playName = msg.Name
}
return m, nil
+ case database.Play:
+ m.isLoading = false
+ m.play = &msg
+ m.err = nil
+ return m, nil
case error:
m.isLoading = false
m.err = msg
@@ -167,13 +177,8 @@ func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m PlayModel) View() string {
formWidth := getFormWidth(m.width)
- // Error message
- formError := ""
- if m.err != nil {
- formError = fmt.Sprintf("Error: %v", m.err.Error())
- }
-
var content string
+ base := lipgloss.NewStyle().Align(lipgloss.Center).Width(m.width)
switch m.page {
case LandingPage:
@@ -181,50 +186,75 @@ func (m PlayModel) View() string {
m.namePrompt.Blur()
case InsertCodePage:
m.namePrompt.Focus()
- content = m.namePrompt.View()
-
- statusMsg := fmt.Sprintf("Press %s to join", lipgloss.NewStyle().Italic(true).Render("Enter"))
+ var statusMsg string
if m.isLoading {
statusMsg = "Loading..."
- }
-
- content = lipgloss.NewStyle().
- Align(lipgloss.Center).
- Width(m.width).
- Render(
- lipgloss.JoinVertical(lipgloss.Left,
- lipgloss.NewStyle().Width(23).Render("Insert play code:"),
- m.namePrompt.View(),
+ content = base.
+ Render(
lipgloss.NewStyle().
Align(lipgloss.Center).
- PaddingTop(2).
- Width(23).
Bold(true).
Render(statusMsg),
- ),
- )
+ )
+ } else if m.play != nil {
+ statusMsg = fmt.Sprintf("You are playing versus %s", lipgloss.NewStyle().Foreground(lipgloss.Color("#e67e22")).Render(m.play.Player1.Username))
+ content = base.
+ Render(
+ lipgloss.NewStyle().
+ Align(lipgloss.Center).
+ Width(m.width).
+ Bold(true).
+ Render(statusMsg),
+ )
+ } else {
+ statusMsg = fmt.Sprintf("Press %s to join", lipgloss.NewStyle().Italic(true).Render("Enter"))
+ content = base.
+ Render(
+ lipgloss.JoinVertical(lipgloss.Left,
+ lipgloss.NewStyle().Width(23).Render("Insert play code:"),
+ m.namePrompt.View(),
+ lipgloss.NewStyle().
+ Align(lipgloss.Center).
+ PaddingTop(2).
+ Width(23).
+ Bold(true).
+ Render(statusMsg),
+ ),
+ )
+ }
+
case StartPlayPage:
- statusMsg := fmt.Sprintf("Share `%s` to your friend", lipgloss.NewStyle().Italic(true).Foreground(lipgloss.Color("#F39C12")).Render(m.playName))
+ var statusMsg string
if m.isLoading {
statusMsg = "Loading..."
+ } else if m.playName != "" {
+ statusMsg = fmt.Sprintf("Share `%s` to your friend", lipgloss.NewStyle().Italic(true).Foreground(lipgloss.Color("#F39C12")).Render(m.playName))
}
- content = lipgloss.NewStyle().
- Align(lipgloss.Center).
- Width(m.width).
+ content = base.
Render(statusMsg)
}
- windowContent := lipgloss.JoinVertical(
- lipgloss.Center,
- windowStyle.
- Width(formWidth).
- Render(lipgloss.JoinVertical(
+ var windowContent string
+ if m.err != nil {
+ formError := fmt.Sprintf("Error: %v", m.err.Error())
+ windowContent = lipgloss.JoinVertical(
+ lipgloss.Center,
+ windowStyle.Width(formWidth).Render(lipgloss.JoinVertical(
lipgloss.Center,
errorStyle.Align(lipgloss.Center).Width(formWidth-4).Render(formError),
content,
)),
- )
+ )
+ } else {
+ windowContent = lipgloss.JoinVertical(
+ lipgloss.Center,
+ windowStyle.Width(formWidth).Render(lipgloss.JoinVertical(
+ lipgloss.Center,
+ content,
+ )),
+ )
+ }
enterKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.EnterNewPlay.Help().Key), m.keys.EnterNewPlay.Help().Desc)
startKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.StartNewPlay.Help().Key), m.keys.StartNewPlay.Help().Desc)
@@ -276,8 +306,13 @@ func (m PlayModel) newPlayCallback() tea.Cmd {
url := os.Getenv("API_BASE") + "/play"
+ port, err := network.GetRandomAvailablePort()
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
payload, err := json.Marshal(map[string]string{
- "ip": network.GetOutboundIP().String(),
+ "ip": fmt.Sprintf("%s:%d", network.GetOutboundIP().String(), port),
})
if err != nil {
@@ -316,3 +351,77 @@ func (m PlayModel) newPlayCallback() tea.Cmd {
return response
}
}
+
+func (m PlayModel) enterPlay() tea.Cmd {
+ return func() tea.Msg {
+ f, err := os.Open(".rahannarc")
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var authorization string
+ for scanner.Scan() {
+ authorization = scanner.Text()
+ }
+
+ if err := scanner.Err(); err != nil {
+ fmt.Println("Error during scanning:", err)
+ }
+
+ url := os.Getenv("API_BASE") + "/enter-play"
+
+ port, err := network.GetRandomAvailablePort()
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ payload, err := json.Marshal(map[string]string{
+ "ip": fmt.Sprintf("%s:%d", network.GetOutboundIP().String(), port),
+ "name": m.namePrompt.Value(),
+ })
+
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ req, err := http.NewRequest("POST", url, bytes.NewReader(payload))
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization))
+
+ client := &http.Client{}
+
+ resp, err := client.Do(req)
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ var response playResponse
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return playResponse{Error: fmt.Sprintf("HTTP error: %d, unable to decode body", resp.StatusCode)}
+ }
+ return playResponse{Error: response.Error}
+ }
+
+ var response database.Play
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)}
+ }
+
+ return response
+ }
+}
+
+func (m PlayModel) logout() tea.Cmd {
+ if err := os.Remove(".rahannarc"); err != nil {
+ return nil
+ }
+ return SwitchModelCmd(NewAuthModel(m.width, m.height+1))
+}