Add widget route

This commit is contained in:
Jacob Gunther
2023-03-30 22:47:01 -05:00
parent c45944b41b
commit eaa1492baa
9 changed files with 254 additions and 10 deletions

BIN
src/Ubuntu-B.ttf Normal file

Binary file not shown.

BIN
src/Ubuntu-R.ttf Normal file

Binary file not shown.

BIN
src/UbuntuMono-R.ttf Normal file

Binary file not shown.

View File

@@ -11,6 +11,7 @@ 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("/icon", DefaultIconHandler)
app.Get("/icon/:address", IconHandler)
@@ -49,6 +50,33 @@ func JavaStatusHandler(ctx *fiber.Ctx) error {
return ctx.JSON(response)
}
// JavaWidgetHandler returns the widget of the Java Edition server.
func JavaWidgetHandler(ctx *fiber.Ctx) error {
host, port, err := ParseAddress(ctx.Params("address"), 25565)
if err != nil {
return ctx.Status(http.StatusBadRequest).SendString("Invalid address value")
}
if err = r.Increment(fmt.Sprintf("java-hits:%s-%d", host, port)); err != nil {
return err
}
response, _, err := GetJavaStatus(host, port)
if err != nil {
return err
}
widget, err := GenerateJavaWidget(response, ctx.QueryBool("dark", true))
if err != nil {
return err
}
return ctx.Type("png").Send(widget)
}
// BedrockStatusHandler returns the status of the Bedrock edition Minecraft server specified in the address parameter.
func BedrockStatusHandler(ctx *fiber.Ctx) error {
host, port, err := ParseAddress(ctx.Params("address"), 19132)
@@ -101,7 +129,7 @@ func IconHandler(ctx *fiber.Ctx) error {
// DefaultIconHandler returns the default server icon.
func DefaultIconHandler(ctx *fiber.Ctx) error {
return ctx.Type("png").Send(defaultIcon)
return ctx.Type("png").Send(defaultIconBytes)
}
// NotFoundHandler handles requests to routes that do not exist and returns a 404 Not Found status.

View File

@@ -187,7 +187,7 @@ func GetServerIcon(host string, port uint16) ([]byte, time.Duration, error) {
return cache, ttl, err
}
icon := defaultIcon
icon := defaultIconBytes
status, err := mcutil.Status(host, port)

View File

@@ -1,25 +1,86 @@
package main
import (
"bytes"
"crypto/sha1"
_ "embed"
"encoding/base64"
"encoding/hex"
"fmt"
"image"
"image/png"
"io"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
)
var (
//go:embed icon.png
defaultIcon []byte
blockedServers *MutexArray = nil
ipAddressRegex *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`)
defaultIconBytes []byte
defaultIcon image.Image
//go:embed Ubuntu-B.ttf
ubuntuBoldFontBytes []byte
ubuntuBoldFont font.Face
//go:embed Ubuntu-R.ttf
ubuntuRegularFontBytes []byte
ubuntuRegularFont font.Face
//go:embed UbuntuMono-R.ttf
ubuntuMonoFontBytes []byte
ubuntuMonoFont font.Face
ubuntuMonoSmallFont font.Face
blockedServers *MutexArray = nil
ipAddressRegex *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`)
)
func init() {
var err error
if defaultIcon, err = png.Decode(bytes.NewReader(defaultIconBytes)); err != nil {
log.Fatalf("Failed to parse default icon: %v", err)
}
ubuntuBold, err := truetype.Parse(ubuntuBoldFontBytes)
if err != nil {
log.Fatalf("Failed to parse Ubuntu Bold font: %v", err)
}
ubuntuBoldFont = truetype.NewFace(ubuntuBold, &truetype.Options{
Size: 36,
})
ubuntuRegular, err := truetype.Parse(ubuntuRegularFontBytes)
if err != nil {
log.Fatalf("Failed to parse Ubuntu Regular font: %v", err)
}
ubuntuRegularFont = truetype.NewFace(ubuntuRegular, &truetype.Options{
Size: 36,
})
ubuntuMono, err := truetype.Parse(ubuntuMonoFontBytes)
if err != nil {
log.Fatalf("Failed to parse Ubuntu Mono Regular font: %v", err)
}
ubuntuMonoFont = truetype.NewFace(ubuntuMono, &truetype.Options{
Size: 36,
})
ubuntuMonoSmallFont = truetype.NewFace(ubuntuMono, &truetype.Options{
Size: 20,
})
}
// MutexArray is a thread-safe array for storing and checking values.
type MutexArray struct {
List []interface{}
@@ -123,3 +184,49 @@ func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
return result[0], uint16(port), nil
}
// EncodePNG encodes an image.Image into a byte array.
func EncodePNG(img image.Image) ([]byte, error) {
buf := &bytes.Buffer{}
err := png.Encode(buf, img)
return buf.Bytes(), err
}
// GetStatusIcon returns the icon of the server if it exists, or returns the default icon.
func GetStatusIcon(response *JavaStatusResponse) (image.Image, error) {
if response == nil || !response.Online || response.Icon == nil {
return defaultIcon, nil
}
data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(*response.Icon, "data:image/png;base64,"))
if err != nil {
return nil, err
}
return png.Decode(bytes.NewReader(data))
}
// ScaleImageNearestNeighbor scales an image using the nearest neighbor method.
// This method is extremely inefficient, but at the small scaling values we are using the performance hit is almost immeasurable.
func ScaleImageNearestNeighbor(img image.Image, sx, sy int) image.Image {
s := img.Bounds().Size()
out := image.NewRGBA(image.Rect(0, 0, s.X*sx, s.Y*sy))
for ix := 0; ix < s.X; ix++ {
for iy := 0; iy < s.Y; iy++ {
c := img.At(ix, iy)
for ox := 0; ox < sx; ox++ {
for oy := 0; oy < sy; oy++ {
out.Set(ix*sx+ox, iy*sy+oy, c)
}
}
}
}
return out
}

95
src/widget.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"fmt"
"image/color"
"github.com/fogleman/gg"
)
const (
// WidgetWidth is the width of the image of the widget.
WidgetWidth = 860
// WidgetHeight is the height of the image of the widget.
WidgetHeight = 240
)
// GenerateJavaWidget generates a widget image from the Java Edition status response.
func GenerateJavaWidget(status *JavaStatusResponse, isDark bool) ([]byte, error) {
var statusColor color.Color = color.RGBA{230, 57, 70, 255}
var statusText string = "Offline"
var players string = "Unknown players"
var address string = status.Host
if status.Online {
statusColor = color.RGBA{46, 204, 113, 255}
statusText = "Online"
if status.Players.Online != nil && status.Players.Max != nil {
players = fmt.Sprintf("%d/%d players", *status.Players.Online, *status.Players.Max)
} else if status.Players.Online != nil {
players = fmt.Sprintf("%d players", *status.Players.Online)
} else {
players = "Unknown players"
}
}
if status.Port != 25565 {
address += fmt.Sprintf(":%d", status.Port)
}
icon, err := GetStatusIcon(status)
if err != nil {
return nil, err
}
ctx := gg.NewContext(WidgetWidth, WidgetHeight)
// Background color
if isDark {
ctx.SetColor(color.Gray{32})
} else {
ctx.SetColor(color.White)
}
ctx.DrawRoundedRectangle(0, 0, WidgetWidth, WidgetHeight, 16)
ctx.Fill()
// Draw server icon
ctx.DrawImage(ScaleImageNearestNeighbor(icon, 3, 3), (WidgetHeight-64*3)/2, (WidgetHeight-64*3)/2)
// Draw status bubble
ctx.SetColor(statusColor)
ctx.DrawCircle(WidgetHeight+8, WidgetHeight/2-55, 8)
ctx.Fill()
// Draw status text
ctx.SetFontFace(ubuntuBoldFont)
ctx.DrawString(statusText, WidgetHeight+28, WidgetHeight/2-44)
ctx.Stroke()
// Draw address text
if isDark {
ctx.SetColor(color.White)
} else {
ctx.SetColor(color.Black)
}
ctx.SetFontFace(ubuntuMonoFont)
ctx.DrawString(address, WidgetHeight, WidgetHeight/2+4)
ctx.Stroke()
// Draw players text
ctx.SetColor(color.Gray{127})
ctx.SetFontFace(ubuntuRegularFont)
ctx.DrawString(players, WidgetHeight, WidgetHeight/2+58)
ctx.Stroke()
// Draw mcstatus.io branding
ctx.SetColor(color.Gray{152})
ctx.SetFontFace(ubuntuMonoSmallFont)
ctx.DrawStringAnchored("© mcstatus.io", WidgetWidth-16, WidgetHeight-38, 1.0, 1.0)
ctx.Stroke()
return EncodePNG(ctx.Image())
}