From 4083e0ab45d1e20ee8c4e0ad62575b5e9a41164c Mon Sep 17 00:00:00 2001 From: bakonpancakz Date: Sun, 24 May 2026 00:59:02 -0700 Subject: [PATCH] Initial Release --- .gitignore | 1 + LICENSE | 21 ++ README.md | 34 +++ README.png | Bin 0 -> 29109 bytes go.mod | 5 + go.sum | 4 + headshot_default.png | Bin 0 -> 540 bytes main.go | 663 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 728 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 headshot_default.png create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a5decc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 bakonpancakz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb0551c --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# ๐Ÿ”Ž `minecraft-discord-status` +See who's online using a fancy Discord Embed! + + + +--- + +## โš™๏ธ Configuration +This application expects a `config.json` in the current working directory with the following fields: + +| Name | Description | +| :-------------- | :-------------------------------------------------------------------- | +| bot_token | Your Discord Bot token, used to send messages and upload emojis | +| bot_channel | The Discord Channel ID to post the server status to | +| server_address | The Address and Port to the Minecraft Server (e.g. `localhost:25565`) | +| update_interval | Update Interval in seconds, default 60 seconds (1 minute) | +| embed_title | The Embed Title, defaults to `Server Status for {minecraft_address}` | +| embed_color | The Embed Color in Hexadecimal Representation (e.g. #FF005B) | + +### Example Configuration +```json +{ + "bot_token": "", + "bot_channel": "954501752722444388", + "server_address": "localhost:25565", + "update_interval": 60, + "embed_color": 16711771, + "embed_title": "Status for __Carthage__" +} +``` + +## ๐Ÿš€ Startup + +Check the Releases tab for a precompiled executable for both Linux and Windows. \ No newline at end of file diff --git a/README.png b/README.png new file mode 100644 index 0000000000000000000000000000000000000000..c63b97515c7d137163f4882fc91c7bb52f5be3a6 GIT binary patch literal 29109 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 literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c0ff3f8 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/bakonpancakz/minecraft-discord-status + +go 1.26 + +require golang.org/x/image v0.39.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d215763 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= diff --git a/headshot_default.png b/headshot_default.png new file mode 100644 index 0000000000000000000000000000000000000000..fd781ae1552d7c5c5be4a6799efd8c085402d151 GIT binary patch literal 540 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sDEfH31!Z9ZwBhK-&sjv*CsZ*SRqJ0(i6e)#?Desq_qh@R<9 zfktN`la`$(F3Bu6MZT~uTEH*J!mE4nUVsU^?^7XB7goV5=^P@yd$n#99c%yITAgWi zKHrkvrCoXY@1meUhEE+E-_BnQo^93YivJx>pbRfVbd?B z{9Ng25?N!?ttP!?cE%>Z3+|ImrAke1M!psFT{azPAYTL6{EPev=gP0YInP}5IK#+( zsss1&()s;wQ>&@Ao<5~<+_OTjE7skroR97 zP}VnW&OBG{=Rbcl`)-TVTzE1n_wW1#Q=v`-8TYJYdCuO8|4rWRzklC;@4}NoFI8sg xy!dt@W!-mu(YNQh|19}yeGSRUC)hviu}`uIJ~2^4vJV)e44$rjF6*2UngH>>>= 7 + if value != 0 { + part |= 0x80 + } + b.WriteByte(part) + if value == 0 { + return + } + } +} + +func readVarInt(r io.Reader) (int, error) { + var result int + for shift := 0; shift < 35; shift += 7 { + var single [1]byte + if _, err := io.ReadFull(r, single[:]); err != nil { + return 0, err + } + result |= int(single[0]&0x7F) << shift + if single[0]&0x80 == 0 { + return result, nil + } + } + return 0, fmt.Errorf("varint too big") +} + +// ---------------------------------------------------------------------------- +// GLOBAL FUNCTIONS +// ---------------------------------------------------------------------------- + +type GlobalOptions struct { + BotToken string `json:"bot_token"` + BotChannelID string `json:"bot_channel"` + BotMessageID string `json:"bot_message"` + ServerAddress string `json:"server_address"` + UpdateInterval int `json:"update_interval"` + EmbedTitle string `json:"embed_title"` + EmbedColor string `json:"embed_color"` + EmbedDecimal int `json:"-"` +} + +func (o *GlobalOptions) Load() error { + + // Parse configuration file + b, err := os.ReadFile(OPTIONS_FILENAME) + if err != nil { + return err + } + if err := json.Unmarshal(b, &o); err != nil { + return err + } + + // Calculate embed color + hex := strings.TrimPrefix(o.EmbedColor, "#") + dec, err := strconv.ParseInt(hex, 16, 32) + o.EmbedDecimal = int(dec) + + // Default values + if o.UpdateInterval == 0 { + o.UpdateInterval = 60 + } + return nil +} + +func (o *GlobalOptions) Save() error { + b, err := json.MarshalIndent(o, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(OPTIONS_FILENAME, b, OPTIONS_FILEMODE); err != nil { + return err + } + return nil +} + +func (o *GlobalOptions) Address() (string, error) { + resultHost, resultPort := "127.0.0.1", 25565 + rawHost, rawPort, hasPort := strings.Cut(o.ServerAddress, ":") + + // Use port if explicity stated + if hasPort { + port, err := strconv.Atoi(rawPort) + if err == nil { + resultHost = rawHost + resultPort = port + return fmt.Sprintf("%s:%d", resultHost, resultPort), nil + } + } + + // Determine port using SRV records (if any) + resultHost = rawHost + if net.ParseIP(rawHost) == nil { + _, addrs, err := net.LookupSRV("minecraft", "tcp", rawHost) + if err == nil && len(addrs) > 0 { + resultHost = strings.TrimSuffix(addrs[0].Target, ".") + resultPort = int(addrs[0].Port) + } + } + + return fmt.Sprintf("%s:%d", resultHost, resultPort), nil +} + +// ---------------------------------------------------------------------------- +// MINECRAFT FUNCTIONS +// ---------------------------------------------------------------------------- + +type MinecraftStatusRoot struct { + Favicon string `json:"favicon"` + Description MinecraftStatusDescription `json:"description"` + Players MinecraftStatusPlayers `json:"players"` +} + +type MinecraftStatusPlayers struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []MinecraftStatusPlayerInfo `json:"sample"` +} + +type MinecraftStatusPlayerInfo struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type MinecraftStatusDescription struct { + Text string +} + +func (d *MinecraftStatusDescription) UnmarshalJSON(data []byte) error { + + // Attempt to parse Chat Components (modern) otherwise use plaintext (legacy) + var builder strings.Builder + var plain string + if err := json.Unmarshal(data, &plain); err != nil { + var component ChatComponent + if err := json.Unmarshal(data, &component); err != nil { + return err + } + plain = component.String() + } + + // Cleanup Special Characters + builder.Grow(len(plain)) + skipNext := false + for _, r := range plain { + if skipNext { + skipNext = false + continue + } + if r == 'ยง' || r == '๏ฟฝ' { + skipNext = true + continue + } + builder.WriteRune(r) + } + + // Cleanup Text Padding + lines := strings.Split(builder.String(), "\n") + maxes := make([]string, 0, len(lines)) + for _, l := range lines { + maxes = append(maxes, strings.TrimSpace(l)) + } + d.Text = strings.Join(maxes, "\n") + + return nil +} + +type ChatComponent struct { + Text string `json:"text"` + Extra []ChatComponent `json:"extra"` + Translate string `json:"translate"` + With []ChatComponent `json:"with"` +} + +func (c ChatComponent) String() string { + var b strings.Builder + + b.WriteString(c.Text) + + if c.Text == "" && c.Translate != "" { + b.WriteString(c.Translate) + } + for _, child := range c.With { + b.WriteString(child.String()) + } + for _, child := range c.Extra { + b.WriteString(child.String()) + } + + return b.String() +} + +func MinecraftActivity() (MinecraftStatusRoot, error) { + var status MinecraftStatusRoot + + // Prepare for TCP Connection + serverAddress, err := OPTIONS.Address() + if err != nil { + return status, err + } + s := strings.SplitN(serverAddress, ":", 2) + v, _ := strconv.Atoi(s[1]) + + serverHost := s[0] + serverPort := uint16(v) + + // Create TCP Socket + var b, p bytes.Buffer + socket, err := net.DialTimeout("tcp", serverAddress, 30*time.Minute) + if err != nil { + return status, err + } + defer socket.Close() + + // Send Handshake + // https://minecraft.wiki/w/Java_Edition_protocol/Packets#Handshake + writeVarInt(&b, 0x00) // Packet ID + writeVarInt(&b, 770) // Protocol Version + writeVarInt(&b, len(serverHost)) // Server Address Length + b.WriteString(serverHost) // Server Address + binary.Write(&b, binary.BigEndian, serverPort) // Server Port + writeVarInt(&b, 1) // Intent (Status) + + writeVarInt(&p, b.Len()) + p.Write(b.Bytes()) + if _, err := socket.Write(p.Bytes()); err != nil { + return status, err + } + p.Reset() + b.Reset() + + // Send Status Request + // https://minecraft.wiki/w/Java_Edition_protocol/Packets#Status_Request + writeVarInt(&b, 0x00) // Status Request + + writeVarInt(&p, b.Len()) + p.Write(b.Bytes()) + if _, err := socket.Write(p.Bytes()); err != nil { + return status, err + } + + // Parse Status + // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response + if _, err := readVarInt(socket); err != nil { + return status, err + } + + packetID, err := readVarInt(socket) + if err != nil { + return status, err + } + if packetID != 0x00 { + return status, fmt.Errorf("unexpected status packet id (%d)", packetID) + } + + payloadLength, err := readVarInt(socket) + if err != nil { + return status, err + } + payload := make([]byte, payloadLength) + if _, err := io.ReadFull(socket, payload); err != nil { + return status, err + } + + if err := json.Unmarshal(payload, &status); err != nil { + return status, err + } + sort.Slice(status.Players.Sample, func(i, j int) bool { + return len(status.Players.Sample[i].Name) > len(status.Players.Sample[j].Name) + }) + + return status, nil +} + +func MinecraftHeadshot(uuid string) ([]byte, error) { + var skinURL string + + // Fetch Profile + uri := "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + res, err := http.Get(uri) + if err != nil { + return nil, err + } + defer res.Body.Close() + + raw, _ := io.ReadAll(res.Body) + if res.StatusCode != 200 { + return nil, fmt.Errorf("request failed with status code %d: %s", res.StatusCode, string(raw)) + } + + // Decode Profile + var Profile struct { + ID string `json:"id"` + Name string `json:"name"` + Properties []struct { + Name string `json:"textures"` + Value string `json:"value"` + } `json:"properties"` + } + if err := json.Unmarshal(raw, &Profile); err != nil { + return nil, err + } + if len(Profile.Properties) < 1 { + return nil, fmt.Errorf("missing required property") + } + + // Decode Profile Properties + rawProps, err := base64.URLEncoding.DecodeString(Profile.Properties[0].Value) + if err != nil { + return nil, fmt.Errorf("invalid or malformed profile properties") + } + var Properties struct { + Timestamp int `json:"timestamp"` + ProfileID string `json:"profileId"` + ProfileName string `json:"profileName"` + Textures struct { + Skin struct { + URL string `json:"url"` + } `json:"SKIN"` + } `json:"textures"` + } + if err := json.Unmarshal(rawProps, &Properties); err != nil { + return nil, err + } + skinURL = Properties.Textures.Skin.URL + if skinURL == "" { + return nil, fmt.Errorf("this player has no skin") + } + + // Download Minecraft Skin + res, err = http.Get(skinURL) + if err != nil { + return nil, err + } + defer res.Body.Close() + + raw, _ = io.ReadAll(res.Body) + if res.StatusCode != 200 { + return nil, fmt.Errorf("request failed with status code %d: %s", res.StatusCode, string(raw)) + } + + // Generate Headshot + skin, err := png.Decode(bytes.NewReader(raw)) + if err != nil { + return nil, err + } + + crop := image.NewRGBA(image.Rect(0, 0, 8, 8)) + draw.Draw(crop, skin.Bounds(), skin, image.Pt(8, 8), draw.Over) // First Layer + draw.Draw(crop, skin.Bounds(), skin, image.Pt(40, 8), draw.Over) // Second Layer + + resize := image.NewRGBA(image.Rect(0, 0, 64, 64)) + draw.NearestNeighbor.Scale(resize, resize.Rect, crop, crop.Rect, draw.Over, nil) + + var b bytes.Buffer + if err := png.Encode(&b, resize); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +// ---------------------------------------------------------------------------- +// DISCORD FUNCTIONS +// ---------------------------------------------------------------------------- + +type DiscordEmote struct { + ID string + Name string +} + +type DiscordUser struct { + ID string + Username string +} + +func DiscordRequest(ctx context.Context, method string, pathname string, body any, v any) error { + var req *http.Request + var err error + + // Generate Request + uri := "https://discord.com/api/v10" + pathname + if body != nil { + // Use Body as JSON Payload + payload, err := json.Marshal(body) + if err != nil { + return err + } + req, err = http.NewRequestWithContext(ctx, method, uri, bytes.NewReader(payload)) + } else { + // Generic Request + req, err = http.NewRequestWithContext(ctx, method, uri, http.NoBody) + } + + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bot "+OPTIONS.BotToken) + + // Check Response + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + b, _ := io.ReadAll(res.Body) + if res.StatusCode < 200 || res.StatusCode > 299 { + return fmt.Errorf("server responded with status code %d: %s", res.StatusCode, string(b)) + } + + // Decode Response (if applicable) + if v != nil { + if err := json.Unmarshal(b, &v); err != nil { + return err + } + } + + return nil +} + +func DiscordCreateEmote(ctx context.Context, name string, data []byte) (DiscordEmote, error) { + var e DiscordEmote + var d = map[string]any{ + // Just use a 'multipart/form-data' discord pls... + "image": ("data:image/png;base64," + base64.StdEncoding.EncodeToString(data)), + "name": name, + } + err := DiscordRequest(ctx, "POST", "/applications/"+PROFILE.ID+"/emojis", d, &e) + return e, err +} + +// ---------------------------------------------------------------------------- +// MAIN LOOP +// ---------------------------------------------------------------------------- + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Load Configuration + if err := OPTIONS.Load(); err != nil { + log.Fatalf("Unable to load options: %s\n", err) + } + + // Load Discord Profile + var RemoteEmotes struct { + Items []DiscordEmote `json:"items"` + } + if err := DiscordRequest(ctx, "GET", "/users/@me", nil, &PROFILE); err != nil { + log.Fatalf("Cannot fetch profile: %s\n", err) + return + } + if err := DiscordRequest(ctx, "GET", "/applications/"+PROFILE.ID+"/emojis", nil, &RemoteEmotes); err != nil { + log.Fatalf("Cannot fetch emotes: %s\n", err) + return + } + EMOTES = RemoteEmotes.Items + + // Find Fallback Emote + for _, emote := range RemoteEmotes.Items { + if emote.Name == EMOTE_HEADSHOT_NAME { + EMOTE_HEADSHOT_ID = emote.ID + } + if emote.Name == EMOTE_ICON_NAME { + EMOTE_ICON_ID = emote.ID + DiscordRequest(ctx, "DELETE", "/applications/"+PROFILE.ID+"/emojis/"+emote.ID, nil, nil) + } + } + if EMOTE_HEADSHOT_ID == "" { + // Create Fallback Emote + newEmoji, err := DiscordCreateEmote(ctx, EMOTE_HEADSHOT_NAME, DEFAULT_HEADSHOT) + if err != nil { + log.Fatalf("Failed to upload default emote: %s\n", err) + return + } + EMOTE_HEADSHOT_ID = newEmoji.ID + EMOTES = append(EMOTES, newEmoji) + } + cancel() + + // Begin Ping Loop + log.Printf("Logged in as '%s' with %d emojis\n", PROFILE.Username, len(EMOTES)) + for { + if err := loop(); err != nil { + log.Printf("Failed to ping server: %s\n", err) + } + if err := OPTIONS.Save(); err != nil { + log.Fatalf("Failed to save options: %s\n", err) + } + time.Sleep(time.Second * time.Duration(OPTIONS.UpdateInterval)) + } +} + +func loop() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Fetch Server Status + payload := map[string]any{} + status, err := MinecraftActivity() + if err != nil { + + // Build Error + log.Println("Failed to fetch server status:", err) + payload["content"] = "Server Unavailable" + payload["embeds"] = []any{} + + } else { + + // Upload Favicon + if status.Favicon != "" && EMOTE_ICON_ID == "" { + fav := strings.TrimPrefix(status.Favicon, "data:image/png;base64,") + raw, err := base64.StdEncoding.DecodeString(fav) + if err != nil { + log.Printf("Failed to decode server favicon: %s\n", err) + goto favicon_finish + } + emote, err := DiscordCreateEmote(ctx, EMOTE_ICON_NAME, raw) + if err != nil { + log.Printf("Failed to upload favicon emote: %s\n", err) + goto favicon_finish + } + EMOTE_ICON_ID = emote.ID + } + favicon_finish: + + // Build Embed + embed := map[string]any{ + "description": fmt.Sprintf( + "```%s```\n**Players Online:** `%d/%d`\n\n", + status.Description.Text, + status.Players.Online, + status.Players.Max, + ), + } + if OPTIONS.EmbedTitle != "" { + embed["title"] = OPTIONS.EmbedTitle + } else { + embed["title"] = fmt.Sprintf("Status for __%s__", OPTIONS.ServerAddress) + } + if OPTIONS.EmbedDecimal != 0 { + embed["color"] = OPTIONS.EmbedDecimal + } + if EMOTE_ICON_ID != "" { + embed["thumbnail"] = map[string]any{ + "url": fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.png", EMOTE_ICON_ID), + } + } + + // Build Player List + playerList := map[string]string{} + for _, p := range status.Players.Sample { + + // Find existing headshot emoji + var emojiID string + emojiName := "player_" + p.Name + for _, e := range EMOTES { + if e.Name == emojiName { + emojiID = e.ID + break + } + } + + // Generate headshot emoji if missing + if emojiID == "" { + headshot, err := MinecraftHeadshot(p.ID) + if err != nil { + log.Printf("Failed to generate headshot for '%s' (%s): %s\n", p.Name, p.ID, err) + playerList[p.Name] = fmt.Sprintf("<:%s:%s>", EMOTE_HEADSHOT_NAME, EMOTE_HEADSHOT_ID) + continue + } + newEmoji, err := DiscordCreateEmote(ctx, emojiName, headshot) + if err != nil { + log.Printf("Failed to upload emoji for '%s': %s\n", p.Name, err) + playerList[p.Name] = fmt.Sprintf("<:%s:%s>", EMOTE_HEADSHOT_NAME, EMOTE_HEADSHOT_ID) + continue + } + emojiID = newEmoji.ID + EMOTES = append(EMOTES, newEmoji) + log.Printf("Generated emoji for player '%s' (%s): %s\n", p.Name, p.ID, emojiID) + } + playerList[p.Name] = fmt.Sprintf("<:%s:%s>", emojiName, emojiID) + } + + // Append players to embed description + for name, emoji := range playerList { + next := fmt.Sprintf("%s%s **%s**\n", embed["description"], emoji, name) + if len(next) > 4096 { + break + } + embed["description"] = next + } + + payload["content"] = "" + payload["embeds"] = []any{embed} + } + + // Post or update status message + if OPTIONS.BotMessageID == "" { + var msg struct { + ID string `json:"id"` + } + if err := DiscordRequest(ctx, "POST", "/channels/"+OPTIONS.BotChannelID+"/messages", payload, &msg); err != nil { + log.Println("Failed to send message:", err) + return err + } + OPTIONS.BotMessageID = msg.ID + } else { + if err := DiscordRequest(ctx, "PATCH", "/channels/"+OPTIONS.BotChannelID+"/messages/"+OPTIONS.BotMessageID, payload, nil); err != nil { + log.Println("Failed to update message:", err) + OPTIONS.BotMessageID = "" + return err + } + } + + return nil +}