Initial Release
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed headshot_default.png
|
||||
DEFAULT_HEADSHOT []byte //
|
||||
OPTIONS_FILENAME = "config.json" //
|
||||
OPTIONS_FILEMODE = os.FileMode(0600) // rw-------
|
||||
OPTIONS GlobalOptions // Global Options
|
||||
PROFILE DiscordUser // Global Profiles
|
||||
EMOTES []DiscordEmote // Global Emotes
|
||||
EMOTE_HEADSHOT_NAME = "util_head"
|
||||
EMOTE_HEADSHOT_ID string
|
||||
EMOTE_ICON_NAME = "util_icon"
|
||||
EMOTE_ICON_ID string
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// HELPER FUNCTIONS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func writeVarInt(b *bytes.Buffer, value int) {
|
||||
for {
|
||||
part := byte(value & 0x7F)
|
||||
value >>= 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
|
||||
}
|
||||
Reference in New Issue
Block a user