From 47788a19f5a5d2c8bd0c8d30c1c83c8e2669caf9 Mon Sep 17 00:00:00 2001 From: riomoo Date: Mon, 20 Oct 2025 10:01:43 -0400 Subject: [PATCH] Full packaged app - LFS (just incase) - WS - Room creation/destruction - Dice Rolling functions - Modifier added correctly --- Dockerfile | 36 +++ LICENSE | 17 ++ README.md | 58 ++++ app/gofudge/main.go | 693 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 + go.sum | 2 + run.sh | 41 +++ 7 files changed, 852 insertions(+) create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/gofudge/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100755 run.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fba9f9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Build stage +FROM golang:bookworm AS builder + +# Install UPX +RUN apt-get update && apt-get install -y wget xz-utils && rm -rf /var/lib/apt/lists/* + +# Download the latest UPX binary directly from GitHub +RUN wget https://github.com/upx/upx/releases/download/v5.0.2/upx-5.0.2-amd64_linux.tar.xz +RUN tar -xf upx-5.0.2-amd64_linux.tar.xz && mv upx-5.0.2-amd64_linux/upx /usr/local/bin/upx && rm -r upx-5.0.2-amd64_linux upx-5.0.2-amd64_linux.tar.xz + +# Create a simple Go web server +WORKDIR /app + +# Copy go mod files first for better layer caching +COPY go.mod ./ +RUN go mod download + +# Copy source code +COPY . . + +# Create necessary directories, build, and compress with UPX +RUN mkdir -p /var/sockets +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-s -w -extldflags '-static' -X main.GOMEMLIMIT=50MiB -X runtime.defaultGOGC=150" -trimpath -gcflags="-l=4" -asmflags=-trimpath -o bin/main app/gofudge/main.go +RUN upx --best --ultra-brute bin/main +RUN chmod +x bin/main + +# Final stage with Chainguard static +FROM cgr.dev/chainguard/static:latest +WORKDIR /app + +# Copy only the built binary and necessary directories +COPY --from=builder /app/bin/main ./bin/main + +EXPOSE 8080 +USER nonroot:nonroot +CMD ["./bin/main"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b35358f --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Copyright (c) 2025 Riomoo [alister at kamikishi dot net] + +This software is licensed under the Prism Information License (PIL). + +- You are free to use, modify, and distribute this software. +- Any derivative work must include this license. +- You must also provide a concise explanation of how to operate this software, + as well as backends and frontends (if any) that work with this software. +- You must credit the original creator of this software. +- You may choose to license this software under additional licenses, provided that the terms of the original PIL are still adhered to. + +This software is provided "as is", without any warranty of any kind, +express or implied, including but not limited to the warranties of merchantability, +fitness for a particular purpose, and non-infringement. + +By using this software, you agree to the terms of the PIL. +For more information visit: https://pil.jester-designs.com/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4d3417 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# GoFudge + +A Fudge Dice rolling room programmed in Go + +## License + +[![Custom badge](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fshare.jester-designs.com%2Fview%2Fpil.json)](LICENSE) + +## Prerequisites + +[![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/dl/) +- The version of **Go** used to test the code in this repository is **1.25.3**. + +## Get started + +- run the following commands: `go mod tidy; go build -o bin/main ./app/gofudge/main.go` then `./bin/main` to start the server on port 8080. +- Visit http://localhost:8080 in your browser. + +- Upon visiting the URL you will be created with a username entry and Create room button. After that you will be in the room. +- (If you are hosting this publicly) You can copy the room link in the top right hand corner and share it to anyone. They will be prompted to also pick a username. +- From there you may increase/decrease the modifier as needed for the skill you are rolling for. + +### Podman/Docker + +- If you want to use this with Docker, replace all `podman` commands in `run.sh` instances with `docker` +- Use `run.sh` script which will start the site on port `12007` and to make it public change the `8080` port in the NGINX config to `12007` or change the port in the `run.sh` script how you like. +- To use it locally, same as above but visit http://localhost:12007 instead. + +## Config for NGINX to use as a website: +``` +upstream gofudge { + server 127.0.0.1:8080; + server [::1]:8080; +} +server { + listen 80; + listen [::1]:80; + server_name fudge.example.com; + location / { + proxy_pass http://gofudge; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +
+ +## Software Used but not included + +![Arch](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=arch-linux&logoColor=fff&style=for-the-badge) +![Vim](https://img.shields.io/badge/VIM-%2311AB00.svg?style=for-the-badge&logo=vim&logoColor=white) +![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) +![Forgejo](https://img.shields.io/badge/forgejo-%23FB923C.svg?style=for-the-badge&logo=forgejo&logoColor=white) + +
diff --git a/app/gofudge/main.go b/app/gofudge/main.go new file mode 100644 index 0000000..11b7336 --- /dev/null +++ b/app/gofudge/main.go @@ -0,0 +1,693 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "html/template" + "log" + "math/big" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +type Room struct { + ID string + Users map[*User]bool + History []RollResult + mu sync.RWMutex +} + +type User struct { + Username string + Conn *websocket.Conn + Room *Room +} + +type RollResult struct { + Username string `json:"username"` + Dice []int `json:"dice"` + Modifier int `json:"modifier"` + Total int `json:"total"` + Timestamp time.Time `json:"timestamp"` +} + +type Message struct { + Type string `json:"type"` + Data interface{} `json:"data"` + Username string `json:"username,omitempty"` +} + +var ( + rooms = make(map[string]*Room) + roomsMu sync.RWMutex + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } +) + +func generateRoomID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} + +func rollFateDice() []int { + dice := make([]int, 4) + for i := 0; i < 4; i++ { + n, _ := rand.Int(rand.Reader, big.NewInt(3)) + dice[i] = int(n.Int64()) - 1 // -1, 0, or 1 + } + return dice +} + +func getOrCreateRoom(roomID string) *Room { + roomsMu.Lock() + defer roomsMu.Unlock() + + if room, exists := rooms[roomID]; exists { + return room + } + + room := &Room{ + ID: roomID, + Users: make(map[*User]bool), + History: []RollResult{}, + } + rooms[roomID] = room + return room +} + +func deleteRoomIfEmpty(roomID string) { + roomsMu.Lock() + defer roomsMu.Unlock() + + if room, exists := rooms[roomID]; exists { + room.mu.RLock() + isEmpty := len(room.Users) == 0 + room.mu.RUnlock() + + if isEmpty { + delete(rooms, roomID) + log.Printf("Room %s deleted", roomID) + } + } +} + +func (r *Room) broadcast(msg Message) { + r.mu.RLock() + defer r.mu.RUnlock() + + data, _ := json.Marshal(msg) + for user := range r.Users { + user.Conn.WriteMessage(websocket.TextMessage, data) + } +} + +func (r *Room) addUser(user *User) { + r.mu.Lock() + r.Users[user] = true + r.mu.Unlock() + + // Send room history to new user + r.mu.RLock() + history := r.History + r.mu.RUnlock() + + historyMsg := Message{Type: "history", Data: history} + data, _ := json.Marshal(historyMsg) + user.Conn.WriteMessage(websocket.TextMessage, data) + + // Notify others + r.broadcast(Message{ + Type: "user_joined", + Username: user.Username, + }) +} + +func (r *Room) removeUser(user *User) { + r.mu.Lock() + delete(r.Users, user) + r.mu.Unlock() + + r.broadcast(Message{ + Type: "user_left", + Username: user.Username, + }) +} + +func handleHome(w http.ResponseWriter, r *http.Request) { + tmpl := ` + + + Go Fudge Dice Roller + + + +
+

Go Fudge Dice Roller

+
+

Create New Room

+ + +
+
+ + + +` + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(tmpl)) +} + +func handleRoom(w http.ResponseWriter, r *http.Request) { + roomID := r.URL.Path[len("/room/"):] + username := r.URL.Query().Get("username") + + if username == "" { + // Show username prompt + tmpl := fmt.Sprintf(` + + + Join Room - FATE + + + +
+

Join Room

+ + +
+ + + +`, roomID) + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(tmpl)) + return + } + + tmpl := template.Must(template.New("room").Parse(` + + + FATE Room + + + +
+
+

Go Fudge Dice Roller

+ +
+ +
+

Roll Dice

+
+ + +
+
+ +
+

Roll History

+
+
+
+ + + + + +`)) + + data := struct { + RoomID string + Username string + }{ + RoomID: roomID, + Username: username, + } + + tmpl.Execute(w, data) +} + +func handleCreateRoom(w http.ResponseWriter, r *http.Request) { + roomID := generateRoomID() + json.NewEncoder(w).Encode(map[string]string{"roomId": roomID}) +} + +func handleWebSocket(w http.ResponseWriter, r *http.Request) { + roomID := r.URL.Query().Get("room") + username := r.URL.Query().Get("username") + + if roomID == "" || username == "" { + http.Error(w, "Missing parameters", http.StatusBadRequest) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + room := getOrCreateRoom(roomID) + user := &User{ + Username: username, + Conn: conn, + Room: room, + } + + room.addUser(user) + defer func() { + room.removeUser(user) + conn.Close() + deleteRoomIfEmpty(roomID) + }() + + for { + var msg Message + err := conn.ReadJSON(&msg) + if err != nil { + break + } + + if msg.Type == "roll" { + modifier := 0 + if m, ok := msg.Data.(map[string]interface{}); ok { + if mod, ok := m["modifier"].(float64); ok { + modifier = int(mod) + } + } + + dice := rollFateDice() + sum := 0 + for _, d := range dice { + sum += d + } + + result := RollResult{ + Username: username, + Dice: dice, + Modifier: modifier, + Total: sum + modifier, + Timestamp: time.Now(), + } + + room.mu.Lock() + room.History = append(room.History, result) + room.mu.Unlock() + + room.broadcast(Message{ + Type: "roll", + Data: result, + }) + } + } +} + +func main() { + http.HandleFunc("/", handleHome) + http.HandleFunc("/room/", handleRoom) + http.HandleFunc("/api/create-room", handleCreateRoom) + http.HandleFunc("/ws", handleWebSocket) + + log.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5794a9a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go-fate + +go 1.25.3 + +require github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..7f72d99 --- /dev/null +++ b/run.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Define variables for your image and container name. +IMAGE_NAME="localhost/gofudge:latest" +CONTAINER_NAME="gofudge" + +# Step 1: Build the new container image. +echo "Building new image: $IMAGE_NAME..." +podman build --force-rm -t "$IMAGE_NAME" . + +# Check if the build was successful before proceeding. +if [ $? -ne 0 ]; then + echo "Image build failed. Exiting script." + exit 1 +fi + +# Step 2: Check if the container is already running. +if podman container exists "$CONTAINER_NAME"; then + echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..." + # Step 3: Stop the existing container. + podman stop "$CONTAINER_NAME" + # Step 4: Remove the stopped container. + podman rm "$CONTAINER_NAME" +fi + +# Step 5: Run a new container from the newly built image. +echo "Starting new container from image: $IMAGE_NAME..." +podman run -d --name "$CONTAINER_NAME" --memory=75m --restart unless-stopped -p 12007:8080 "$IMAGE_NAME" + +# Check if the new container started successfully. +if [ $? -ne 0 ]; then + echo "Failed to start new container. Exiting script." + exit 1 +fi + +# Step 6: Clean up old, unused images. +# This command removes any images that are not being used by a container. +echo "Cleaning up old images..." +podman image prune --force + +echo "Update and cleanup complete!"