Support query lookup for Java servers
This commit is contained in:
parent
2ea126daed
commit
30311f200a
2
go.mod
2
go.mod
@ -7,7 +7,7 @@ require (
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/gofiber/fiber/v2 v2.43.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/mcstatus-io/mcutil v0.0.0-20230416213822-e8ba7a2ea0dd
|
||||
github.com/mcstatus-io/mcutil v1.0.0
|
||||
golang.org/x/image v0.6.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
6
go.sum
6
go.sum
@ -24,10 +24,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mcstatus-io/mcutil v0.0.0-20230331012121-a2cb94e8ebfe h1:hbmOZmXWQgx+R7RTL4V0ORTEn4GNp2/+cKcqyz4k9yU=
|
||||
github.com/mcstatus-io/mcutil v0.0.0-20230331012121-a2cb94e8ebfe/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
|
||||
github.com/mcstatus-io/mcutil v0.0.0-20230416213822-e8ba7a2ea0dd h1:RL36cvcSw53zICdg0CtPD72RuUUH/MgwcEaLz6pE84s=
|
||||
github.com/mcstatus-io/mcutil v0.0.0-20230416213822-e8ba7a2ea0dd/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
|
||||
github.com/mcstatus-io/mcutil v1.0.0 h1:QoULNHXbzYLC2PN1OG3rX4I8AXUivvTFjSpbf5aQqcQ=
|
||||
github.com/mcstatus-io/mcutil v1.0.0/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
|
||||
@ -11,8 +11,8 @@ import (
|
||||
func init() {
|
||||
app.Get("/ping", PingHandler)
|
||||
app.Get("/status/java/:address", JavaStatusHandler)
|
||||
app.Get("/widget/java/:address", JavaWidgetHandler)
|
||||
app.Get("/status/bedrock/:address", BedrockStatusHandler)
|
||||
app.Get("/widget/java/:address", JavaWidgetHandler)
|
||||
app.Get("/icon", DefaultIconHandler)
|
||||
app.Get("/icon/:address", IconHandler)
|
||||
app.Use(NotFoundHandler)
|
||||
|
||||
330
src/status.go
330
src/status.go
@ -4,10 +4,15 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mcstatus-io/mcutil"
|
||||
"github.com/mcstatus-io/mcutil/description"
|
||||
"github.com/mcstatus-io/mcutil/options"
|
||||
"github.com/mcstatus-io/mcutil/response"
|
||||
)
|
||||
|
||||
// StatusResponse is the root response for returning any status response from the API.
|
||||
@ -28,11 +33,13 @@ type JavaStatusResponse struct {
|
||||
|
||||
// JavaStatus is the status response properties for Java Edition.
|
||||
type JavaStatus struct {
|
||||
Version *JavaVersion `json:"version"`
|
||||
Players JavaPlayers `json:"players"`
|
||||
MOTD MOTD `json:"motd"`
|
||||
Icon *string `json:"icon"`
|
||||
Mods []Mod `json:"mods"`
|
||||
Version *JavaVersion `json:"version"`
|
||||
Players JavaPlayers `json:"players"`
|
||||
MOTD MOTD `json:"motd"`
|
||||
Icon *string `json:"icon"`
|
||||
Mods []Mod `json:"mods"`
|
||||
Software *string `json:"software"`
|
||||
Plugins []Plugin `json:"plugins"`
|
||||
}
|
||||
|
||||
// BedrockStatusResponse is the combined response of the root response and the Bedrock Edition status response.
|
||||
@ -99,6 +106,12 @@ type Mod struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Plugin is a plugin that is enabled on a Java Edition server.
|
||||
type Plugin struct {
|
||||
Name string `json:"name"`
|
||||
Version *string `json:"version"`
|
||||
}
|
||||
|
||||
// GetJavaStatus returns the status response of a Java Edition server, either using cache or fetching a fresh status.
|
||||
func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration, error) {
|
||||
cacheKey := fmt.Sprintf("java:%s-%d", host, port)
|
||||
@ -117,11 +130,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration
|
||||
return &response, ttl, err
|
||||
}
|
||||
|
||||
response, err := FetchJavaStatus(host, port)
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
response := FetchJavaStatus(host, port)
|
||||
|
||||
data, err := json.Marshal(response)
|
||||
|
||||
@ -133,7 +142,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return response, 0, nil
|
||||
return &response, 0, nil
|
||||
}
|
||||
|
||||
// GetBedrockStatus returns the status response of a Bedrock Edition server, either using cache or fetching a fresh status.
|
||||
@ -208,118 +217,36 @@ func GetServerIcon(host string, port uint16) ([]byte, time.Duration, error) {
|
||||
return icon, 0, nil
|
||||
}
|
||||
|
||||
// FetchJavaStatus fetches a fresh status of a Java Edition server.
|
||||
func FetchJavaStatus(host string, port uint16) (*JavaStatusResponse, error) {
|
||||
status, err := mcutil.Status(host, port)
|
||||
// FetchJavaStatus fetches fresh information about a Java Edition Minecraft server.
|
||||
func FetchJavaStatus(host string, port uint16) JavaStatusResponse {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if err != nil {
|
||||
statusLegacy, err := mcutil.StatusLegacy(host, port)
|
||||
wg.Add(2)
|
||||
|
||||
if err != nil {
|
||||
return &JavaStatusResponse{
|
||||
StatusResponse: StatusResponse{
|
||||
Online: false,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
RetrievedAt: time.Now().UnixMilli(),
|
||||
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
|
||||
},
|
||||
}, nil
|
||||
var status interface{} = nil
|
||||
var query *response.FullQuery = nil
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if result, _ := mcutil.Status(host, port); result != nil {
|
||||
status = result
|
||||
} else if result, _ := mcutil.StatusLegacy(host, port); result != nil {
|
||||
status = result
|
||||
}
|
||||
}()
|
||||
|
||||
response := &JavaStatusResponse{
|
||||
StatusResponse: StatusResponse{
|
||||
Online: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
RetrievedAt: time.Now().UnixMilli(),
|
||||
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
|
||||
},
|
||||
JavaStatus: &JavaStatus{
|
||||
Version: nil,
|
||||
Players: JavaPlayers{
|
||||
Online: &statusLegacy.Players.Online,
|
||||
Max: &statusLegacy.Players.Max,
|
||||
List: make([]Player, 0),
|
||||
},
|
||||
MOTD: MOTD{
|
||||
Raw: statusLegacy.MOTD.Raw,
|
||||
Clean: statusLegacy.MOTD.Clean,
|
||||
HTML: statusLegacy.MOTD.HTML,
|
||||
},
|
||||
Icon: nil,
|
||||
Mods: make([]Mod, 0),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if statusLegacy.Version != nil {
|
||||
response.Version = &JavaVersion{
|
||||
NameRaw: statusLegacy.Version.NameRaw,
|
||||
NameClean: statusLegacy.Version.NameClean,
|
||||
NameHTML: statusLegacy.Version.NameHTML,
|
||||
Protocol: statusLegacy.Version.Protocol,
|
||||
}
|
||||
}
|
||||
query, _ = mcutil.FullQuery(host, port, options.Query{
|
||||
Timeout: time.Second,
|
||||
})
|
||||
}()
|
||||
|
||||
return response, nil
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
playerList := make([]Player, 0)
|
||||
|
||||
if status.Players.Sample != nil {
|
||||
for _, player := range status.Players.Sample {
|
||||
playerList = append(playerList, Player{
|
||||
UUID: player.ID,
|
||||
NameRaw: player.NameRaw,
|
||||
NameClean: player.NameClean,
|
||||
NameHTML: player.NameHTML,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
modList := make([]Mod, 0)
|
||||
|
||||
if status.ModInfo != nil {
|
||||
for _, mod := range status.ModInfo.Mods {
|
||||
modList = append(modList, Mod{
|
||||
Name: mod.ID,
|
||||
Version: mod.Version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &JavaStatusResponse{
|
||||
StatusResponse: StatusResponse{
|
||||
Online: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
RetrievedAt: time.Now().UnixMilli(),
|
||||
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
|
||||
},
|
||||
JavaStatus: &JavaStatus{
|
||||
Version: &JavaVersion{
|
||||
NameRaw: status.Version.NameRaw,
|
||||
NameClean: status.Version.NameClean,
|
||||
NameHTML: status.Version.NameHTML,
|
||||
Protocol: status.Version.Protocol,
|
||||
},
|
||||
Players: JavaPlayers{
|
||||
Online: status.Players.Online,
|
||||
Max: status.Players.Max,
|
||||
List: playerList,
|
||||
},
|
||||
MOTD: MOTD{
|
||||
Raw: status.MOTD.Raw,
|
||||
Clean: status.MOTD.Clean,
|
||||
HTML: status.MOTD.HTML,
|
||||
},
|
||||
Icon: status.Favicon,
|
||||
Mods: modList,
|
||||
},
|
||||
}, nil
|
||||
return BuildJavaResponse(host, port, status, query)
|
||||
}
|
||||
|
||||
// FetchBedrockStatus fetches a fresh status of a Bedrock Edition server.
|
||||
@ -412,3 +339,174 @@ func FetchBedrockStatus(host string, port uint16) (*BedrockStatusResponse, error
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// BuildJavaResponse builds the response data from the status and query information.
|
||||
func BuildJavaResponse(host string, port uint16, status interface{}, query *response.FullQuery) (result JavaStatusResponse) {
|
||||
result = JavaStatusResponse{
|
||||
StatusResponse: StatusResponse{
|
||||
Online: status != nil || query != nil,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
RetrievedAt: time.Now().UnixMilli(),
|
||||
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
|
||||
},
|
||||
JavaStatus: nil,
|
||||
}
|
||||
|
||||
if status == nil && query == nil {
|
||||
return
|
||||
}
|
||||
|
||||
result.JavaStatus = &JavaStatus{
|
||||
Players: JavaPlayers{
|
||||
List: make([]Player, 0),
|
||||
},
|
||||
Mods: make([]Mod, 0),
|
||||
Plugins: make([]Plugin, 0),
|
||||
}
|
||||
|
||||
if status != nil {
|
||||
switch s := status.(type) {
|
||||
case *response.JavaStatus:
|
||||
{
|
||||
result.Version = &JavaVersion{
|
||||
NameRaw: s.Version.NameRaw,
|
||||
NameClean: s.Version.NameClean,
|
||||
NameHTML: s.Version.NameHTML,
|
||||
Protocol: s.Version.Protocol,
|
||||
}
|
||||
|
||||
result.Players = JavaPlayers{
|
||||
Online: s.Players.Online,
|
||||
Max: s.Players.Max,
|
||||
List: make([]Player, 0),
|
||||
}
|
||||
|
||||
result.MOTD = MOTD{
|
||||
Raw: s.MOTD.Raw,
|
||||
Clean: s.MOTD.Clean,
|
||||
HTML: s.MOTD.HTML,
|
||||
}
|
||||
|
||||
result.Icon = s.Favicon
|
||||
|
||||
if s.Players.Sample != nil {
|
||||
for _, player := range s.Players.Sample {
|
||||
result.Players.List = append(result.Players.List, Player{
|
||||
UUID: player.ID,
|
||||
NameRaw: player.NameRaw,
|
||||
NameClean: player.NameClean,
|
||||
NameHTML: player.NameHTML,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if s.ModInfo != nil {
|
||||
for _, mod := range s.ModInfo.Mods {
|
||||
result.Mods = append(result.Mods, Mod{
|
||||
Name: mod.ID,
|
||||
Version: mod.Version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case *response.JavaStatusLegacy:
|
||||
{
|
||||
if s.Version != nil {
|
||||
result.Version = &JavaVersion{
|
||||
NameRaw: s.Version.NameRaw,
|
||||
NameClean: s.Version.NameClean,
|
||||
NameHTML: s.Version.NameHTML,
|
||||
Protocol: s.Version.Protocol,
|
||||
}
|
||||
}
|
||||
|
||||
result.Players = JavaPlayers{
|
||||
Online: &s.Players.Online,
|
||||
Max: &s.Players.Max,
|
||||
List: make([]Player, 0),
|
||||
}
|
||||
|
||||
result.MOTD = MOTD{
|
||||
Raw: s.MOTD.Raw,
|
||||
Clean: s.MOTD.Clean,
|
||||
HTML: s.MOTD.HTML,
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unknown status type: %T", status))
|
||||
}
|
||||
}
|
||||
|
||||
if query != nil {
|
||||
if status == nil {
|
||||
if motd, ok := query.Data["hostname"]; ok {
|
||||
if parsedMOTD, err := description.ParseFormatting(motd); err == nil {
|
||||
result.MOTD = MOTD{
|
||||
Raw: parsedMOTD.Raw,
|
||||
Clean: parsedMOTD.Clean,
|
||||
HTML: parsedMOTD.HTML,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if onlinePlayers, ok := query.Data["numplayers"]; ok {
|
||||
value, err := strconv.ParseInt(onlinePlayers, 10, 64)
|
||||
|
||||
if err == nil {
|
||||
result.Players.Online = &value
|
||||
}
|
||||
}
|
||||
|
||||
if maxPlayers, ok := query.Data["maxplayers"]; ok {
|
||||
value, err := strconv.ParseInt(maxPlayers, 10, 64)
|
||||
|
||||
if err == nil {
|
||||
result.Players.Max = &value
|
||||
}
|
||||
}
|
||||
|
||||
for _, playerName := range query.Players {
|
||||
parsedName, err := description.ParseFormatting(playerName)
|
||||
|
||||
if err == nil {
|
||||
result.Players.List = append(result.Players.List, Player{
|
||||
UUID: "",
|
||||
NameRaw: parsedName.Raw,
|
||||
NameClean: parsedName.Clean,
|
||||
NameHTML: parsedName.HTML,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plugins, ok := query.Data["plugins"]; ok {
|
||||
if softwareSplit := strings.Split(strings.Trim(plugins, " "), ":"); len(softwareSplit) > 1 {
|
||||
result.Software = PointerOf(strings.Trim(softwareSplit[0], " "))
|
||||
|
||||
for _, plugin := range strings.Split(softwareSplit[1], ";") {
|
||||
pluginSplit := strings.Split(strings.Trim(plugin, " "), " ")
|
||||
|
||||
if len(pluginSplit) > 1 {
|
||||
result.Plugins = append(result.Plugins, Plugin{
|
||||
Name: pluginSplit[0],
|
||||
Version: PointerOf(pluginSplit[1]),
|
||||
})
|
||||
} else {
|
||||
result.Plugins = append(result.Plugins, Plugin{
|
||||
Name: pluginSplit[0],
|
||||
Version: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -230,3 +230,8 @@ func ScaleImageNearestNeighbor(img image.Image, sx, sy int) image.Image {
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// PointerOf returns a pointer of the argument passed.
|
||||
func PointerOf[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user