diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c7b885c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,44 @@ +name: Deploy + +on: + push: + branches: + - master + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Lint + uses: actions-contrib/golangci-lint@master + with: + args: run + - name: Setup Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + - name: Get dependencies + run: go mod download + - name: Build + run: go build -v . + - name: Heroku Login + uses: actions/heroku@master + env: + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + with: + args: container:login + - name: Heroku Push + uses: actions/heroku@master + env: + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + with: + args: container:push -a npqueue web + - name: Heroku Release + uses: actions/heroku@master + env: + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + with: + args: container:release -a npqueue web diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index e8f366d..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Go -on: [push] -jobs: - - build: - name: Build - runs-on: ubuntu-latest - steps: - - - name: Set up Go 1.12 - uses: actions/setup-go@v1 - with: - go-version: 1.12 - id: go - - - name: Check out code - uses: actions/checkout@v1 - - - name: Download and install golangci-lint - run: | - curl -L https://github.com/golangci/golangci-lint/releases/download/v1.17.1/golangci-lint-1.17.1-linux-amd64.tar.gz | tar xzf - -C /tmp - sudo mv /tmp/golangci-lint-1.17.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint - - - name: Run the linter - run: golangci-lint run - - - name: Install deps - run: go mod download - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Build - run: go build -v . - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bfa9d5e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.13 AS builder + +WORKDIR /src +COPY . /src + +RUN CGO_ENABLED=0 GOOS=linux go build -o npqueue . + +# --- + +FROM heroku/heroku:18 + +WORKDIR /app +COPY --from=builder /src /app + +ENV GIN_MODE=release + +CMD ["./npqueue"] diff --git a/README.md b/README.md index 876ed90..3ffbf64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [NoPixel Queue List](https://np.pogge.rs/) 👾 -[![Build Status](https://travis-ci.org/jakejarvis/npqueue.svg?branch=master)](https://travis-ci.org/jakejarvis/npqueue) +![](https://github.com/jakejarvis/npqueue/workflows/Deploy/badge.svg) Active and queued player list for [NoPixel GTA5 RP](https://www.nopixel.net/) server. Based on [@shivammeh/nplists3](https://github.com/shivammeh/nplists3). diff --git a/main.go b/main.go index 32c03e3..9dde470 100644 --- a/main.go +++ b/main.go @@ -1,243 +1,242 @@ package main import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "log" - "net" - "net/http" - "os" - "strconv" - "strings" - "time" + "bufio" + "bytes" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" - "github.com/gin-gonic/contrib/static" - "github.com/gin-contrib/secure" - "github.com/gin-gonic/gin" - _ "github.com/heroku/x/hmetrics/onload" + "github.com/gin-gonic/contrib/static" + "github.com/gin-gonic/gin" + _ "github.com/heroku/x/hmetrics/onload" ) type ServerDetailsStruct struct { - Players `json:"players"` - ServerQueue + Players `json:"players"` + ServerQueue } type ServerQueue struct { - CurrentPlayers int64 `json:"currentPlayers"` - CurrentQueue int64 `json:"currentQueue"` + CurrentPlayers int64 `json:"currentPlayers"` + CurrentQueue int64 `json:"currentQueue"` } type Players []Player // Player details type Player struct { - ID int64 `json:"id"` - Identifiers []string `json:"identifiers"` - Name string `json:"name"` - Ping int64 `json:"ping"` + ID int64 `json:"id"` + Identifiers []string `json:"identifiers"` + Name string `json:"name"` + Ping int64 `json:"ping"` } type Nopixeldata []NoPixelPlayer type NoPixelPlayer struct { - ID int `json:"id"` - Name string `json:"name"` - NoPixelID string `json:"noPixelID"` - SteamID string `json:"steamID"` - Twitch string `json:"twitch"` + ID int `json:"id"` + Name string `json:"name"` + NoPixelID string `json:"noPixelID"` + SteamID string `json:"steamID"` + Twitch string `json:"twitch"` } var ( - jsonGet = &http.Client{Timeout: 10 * time.Second} - // Using an environment variable to protect IP - ServerAddress = os.Getenv("SERVER_IP") - // ServerDetails struct to hold PlayerList & ServerDetails struct - ServerDetails = &ServerDetailsStruct{} - // NoPixelData struct - NoPixelData Nopixeldata + jsonGet = &http.Client{Timeout: 10 * time.Second} + // Using an environment variable to protect IP + ServerAddress = os.Getenv("SERVER_IP") + // ServerDetails struct to hold PlayerList & ServerDetails struct + ServerDetails = &ServerDetailsStruct{} + // NoPixelData struct + NoPixelData Nopixeldata ) // getPlayerList sends HTTP get request to get list of players from /players.json func getPlayerList() (err error) { - server := strings.Builder{} - fmt.Fprintf(&server, "http://%s/players.json", ServerAddress) + server := strings.Builder{} + fmt.Fprintf(&server, "http://%s/players.json", ServerAddress) - req, err := jsonGet.Get(server.String()) - if err != nil { - return err - } - defer req.Body.Close() + req, err := jsonGet.Get(server.String()) + if err != nil { + return err + } + defer req.Body.Close() - err = json.NewDecoder(req.Body).Decode(&ServerDetails.Players) - if err != nil { - return err - } + err = json.NewDecoder(req.Body).Decode(&ServerDetails.Players) + if err != nil { + return err + } - return + return } // getServerQueue opens UDP socket to get queue count func getServerQueue() (err error) { - serverData := make([]byte, 256) - serverConnection, err := net.Dial("udp", ServerAddress) - if err != nil { - return err - } else { - defer serverConnection.Close() - } + serverData := make([]byte, 256) + serverConnection, err := net.Dial("udp", ServerAddress) + if err != nil { + return err + } else { + defer serverConnection.Close() + } - // UDP voodoo to get server info -- https://github.com/LiquidObsidian/fivereborn-query/blob/master/index.js#L54 - fmt.Fprintf(serverConnection, "\xFF\xFF\xFF\xFFgetinfo f") - _, err = bufio.NewReader(serverConnection).Read(serverData) + // UDP voodoo to get server info -- https://github.com/LiquidObsidian/fivereborn-query/blob/master/index.js#L54 + fmt.Fprintf(serverConnection, "\xFF\xFF\xFF\xFFgetinfo f") + _, err = bufio.NewReader(serverConnection).Read(serverData) - if err == nil { - serverData := bytes.Split(serverData, []byte("\n")) - serverDetails := bytes.Split(serverData[1], []byte("\\")) - serverQueue := bytes.FieldsFunc(serverDetails[12], func(c rune) bool { return c == '[' || c == ']' }) + if err == nil { + serverData := bytes.Split(serverData, []byte("\n")) + serverDetails := bytes.Split(serverData[1], []byte("\\")) + serverQueue := bytes.FieldsFunc(serverDetails[12], func(c rune) bool { return c == '[' || c == ']' }) - currentPlayerValues, _ := strconv.ParseInt(string(serverDetails[4]), 0, 64) - currentserverQueueValues, _ := strconv.ParseInt(string(serverQueue[0]), 0, 64) - ServerDetails.ServerQueue.CurrentPlayers = currentPlayerValues - - if currentserverQueueValues >= 1 { - ServerDetails.ServerQueue.CurrentQueue = currentserverQueueValues - } - } else { - return err - } + currentPlayerValues, _ := strconv.ParseInt(string(serverDetails[4]), 0, 64) + currentserverQueueValues, _ := strconv.ParseInt(string(serverQueue[0]), 0, 64) + ServerDetails.ServerQueue.CurrentPlayers = currentPlayerValues - return + if currentserverQueueValues >= 1 { + ServerDetails.ServerQueue.CurrentQueue = currentserverQueueValues + } + } else { + return err + } + + return } func steam64toSteam(input int64) (steamid string) { - legacySteamid := ((input - 76561197960265728) / 2) - steamid = fmt.Sprintf("STEAM_0:%d:%d", (input % 2), legacySteamid) + legacySteamid := ((input - 76561197960265728) / 2) + steamid = fmt.Sprintf("STEAM_0:%d:%d", (input % 2), legacySteamid) - return + return } func parsePlayers() (err error) { - var steamIDs []string - for i, v := range ServerDetails.Players { - steamIDs = nil - for ii, vv := range v.Identifiers { - if ii == 0 { - hexID := strings.Replace(vv, "steam:", "0x", -1) - steamID, _ := strconv.ParseInt(hexID, 0, 64) - s := strconv.FormatInt(steamID, 10) - p := getPlayerNoPixelInformation(s) + var steamIDs []string + for i, v := range ServerDetails.Players { + steamIDs = nil + for ii, vv := range v.Identifiers { + if ii == 0 { + hexID := strings.Replace(vv, "steam:", "0x", -1) + steamID, _ := strconv.ParseInt(hexID, 0, 64) + s := strconv.FormatInt(steamID, 10) + p := getPlayerNoPixelInformation(s) - steamIDs = append(steamIDs, - p.Name, - steam64toSteam(steamID), - fmt.Sprintf("%d", steamID), - p.Twitch, - p.NoPixelID) - } - } - ServerDetails.Players[i].Identifiers = steamIDs - } + steamIDs = append(steamIDs, + p.Name, + steam64toSteam(steamID), + fmt.Sprintf("%d", steamID), + p.Twitch, + p.NoPixelID) + } + } + ServerDetails.Players[i].Identifiers = steamIDs + } - return + return } func loadPlayersJSON() (err error) { - jsonFile, err := jsonGet.Get("https://raw.githubusercontent.com/jakejarvis/npqueue/master/directory.json") - if err != nil { - return - } + jsonFile, err := jsonGet.Get("https://raw.githubusercontent.com/jakejarvis/npqueue/master/directory.json") + if err != nil { + return + } - err = json.NewDecoder(jsonFile.Body).Decode(&NoPixelData) - if err != nil { - return err - } + err = json.NewDecoder(jsonFile.Body).Decode(&NoPixelData) + if err != nil { + return err + } - return + return } func getPlayerNoPixelInformation(id string) (p NoPixelPlayer) { - for i := range NoPixelData { - if NoPixelData[i].SteamID == id { - return NoPixelData[i] - } - } + for i := range NoPixelData { + if NoPixelData[i].SteamID == id { + return NoPixelData[i] + } + } - return + return } // List handler for /api/list route func ListHandler(c *gin.Context) { - // Load players JSON - err := loadPlayersJSON() - if err != nil { - log.Fatalf("Failed to load players JSON: %v", err) - return - } + // Load players JSON + err := loadPlayersJSON() + if err != nil { + log.Fatalf("Failed to load players JSON: %v", err) + return + } - // Get player list - err = getPlayerList() - if err != nil { - log.Fatalf("Failed to get player list: %v", err) - return - } + // Get player list + err = getPlayerList() + if err != nil { + log.Fatalf("Failed to get player list: %v", err) + return + } - // Get server queue count - err = getServerQueue() - if err != nil { - log.Fatalf("Failed to get server queue count: %v", err) - return - } + // Get server queue count + err = getServerQueue() + if err != nil { + log.Fatalf("Failed to get server queue count: %v", err) + return + } - // Parse players JSON - err = parsePlayers() - if err != nil { - log.Fatalf("Failed to parse players JSON: %v", err) - return - } + // Parse players JSON + err = parsePlayers() + if err != nil { + log.Fatalf("Failed to parse players JSON: %v", err) + return + } - c.Header("Content-Type", "application/json") - c.Header("Access-Control-Allow-Origin", "*") + c.Header("Content-Type", "application/json") + c.Header("Access-Control-Allow-Origin", "*") - c.JSON(http.StatusOK, ServerDetails) + c.JSON(http.StatusOK, ServerDetails) } func main() { - port := ":" + os.Getenv("PORT") - router := gin.Default() + port := ":" + os.Getenv("PORT") + router := gin.Default() + /* + router.Use(secure.New(secure.Config{ + SSLRedirect: true, + SSLHost: "np.pogge.rs", + STSSeconds: 315360000, + STSIncludeSubdomains: false, + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXssFilter: true, + SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, + })) + */ + // Serve frontend static files + router.Use(static.Serve("/", static.LocalFile("./public", true))) - router.Use(secure.New(secure.Config{ - SSLRedirect: true, - SSLHost: "np.pogge.rs", - STSSeconds: 315360000, - STSIncludeSubdomains: false, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXssFilter: true, - SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, - })) + // Setup route group for the API + api := router.Group("/api") + { + api.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "Nothing to see here.", + }) + }) + } - // Serve frontend static files - router.Use(static.Serve("/", static.LocalFile("./public", true))) + // List handler for /api/list + api.GET("/list", ListHandler) - // Setup route group for the API - api := router.Group("/api") - { - api.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H { - "message": "Nothing to see here.", - }) - }) - } - - // List handler for /api/list - api.GET("/list", ListHandler) - - // Run the Gin router - if err := router.Run(port); err != nil { - log.Fatalf("Gin fatal error: %v", err) - } else { - log.Printf("Listening on %s...\n", port) - } -} \ No newline at end of file + // Run the Gin router + if err := router.Run(port); err != nil { + log.Fatalf("Gin fatal error: %v", err) + } else { + log.Printf("Listening on %s...\n", port) + } +}