From e176c80db76b21156f87bfea1a04acd18186ff14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Ryb=C3=A1rsky?= Date: Fri, 23 Aug 2024 12:19:44 +0200 Subject: [PATCH] Test --- .gitignore | 7 +- .idea/sqldialects.xml | 6 + go.mod | 4 +- go.sum | 6 +- main.go | 22 ++ packetcreator.go | 30 +- packetsender.go | 48 ++- response.go | 32 +- saver.go | 693 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 804 insertions(+), 44 deletions(-) create mode 100644 .idea/sqldialects.xml create mode 100644 saver.go diff --git a/.gitignore b/.gitignore index 47b2a32..d5fccf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ -out out/ -out/* \ No newline at end of file +out/* +secrets/.myconnectionstring +secrets +secrets/* +dataSources.xml \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..63772a3 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index 7f75730..ab991ec 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module mcpingquick go 1.23.0 require ( - github.com/Tnze/go-mc v1.20.2 + github.com/go-sql-driver/mysql v1.8.1 github.com/google/uuid v1.6.0 ) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/go.sum b/go.sum index f5c0fec..999c59c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ -github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q= -github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/main.go b/main.go index dc45a3e..fb72b6e 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,11 @@ package main import ( + "database/sql" "encoding/json" "fmt" + _ "github.com/go-sql-driver/mysql" + "log" "os" ) @@ -14,6 +17,25 @@ func main() { fmt.Println("Ty debil") fmt.Println(err) } + connectionBytes, err := os.ReadFile("secrets/.myconnectionstring") + if err != nil { + return + } + db, err := sql.Open("mysql", string(connectionBytes)) + if err != nil { + log.Fatal(err) + } + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + log.Fatal(err) + } + }(db) + err = saveResponse(db, resp) + if err != nil { + log.Fatal(err) + return + } // Pretty print the response respJson, err := json.MarshalIndent(resp, "", " ") if err != nil { diff --git a/packetcreator.go b/packetcreator.go index b3715b1..58f7461 100644 --- a/packetcreator.go +++ b/packetcreator.go @@ -86,6 +86,7 @@ func readPacket(reader *bufio.Reader, threshold int32) (packetID int32, packetDa } n, packetID = receiveVarint(packetData) + packetLength = len(packetData) packetLength -= n packetData = packetData[n:] @@ -94,37 +95,38 @@ func readPacket(reader *bufio.Reader, threshold int32) (packetID int32, packetDa func createPacket(packetID int32, packetData []byte, threshold int32, startedCompression bool) ([]byte, error) { var dataBuffer []byte - addVarint(&dataBuffer, packetID) - - dataBuffer = append(dataBuffer, packetData...) + addVarint(&dataBuffer, packetID) // Add the Packet ID as a VarInt + dataBuffer = append(dataBuffer, packetData...) // Append the packet data var outBuffer []byte - length := int32(len(dataBuffer)) + var outTempBuffer []byte + length := int32(len(dataBuffer)) // Get the length of the uncompressed packet if startedCompression { if threshold > 0 && length >= threshold { - // Compress the packet + // Compress the packet if the length is greater than or equal to the threshold compressedData, err := compress(dataBuffer) if err != nil { return nil, err } dataBuffer = compressedData - // Add the uncompressed length - addVarint(&outBuffer, int32(len(packetData))) + // Add the uncompressed length to outTempBuffer + addVarint(&outTempBuffer, length) } else { - // Set Data Length to 0 if not compressed - addVarint(&outBuffer, 0) + // If not compressing (length < threshold), set Data Length to 0 + addVarint(&outTempBuffer, 0) } } - addVarint(&outBuffer, int32(len(dataBuffer))) + // Append the (compressed or uncompressed) data buffer to outTempBuffer + outTempBuffer = append(outTempBuffer, dataBuffer...) - // Append compressed or uncompressed data - outBuffer = append(outBuffer, dataBuffer...) - - // Add the Packet Length + // Add the total packet length to outBuffer + addVarint(&outBuffer, int32(len(outTempBuffer))) + outBuffer = append(outBuffer, outTempBuffer...) + // Return the final packet data return outBuffer, nil } diff --git a/packetsender.go b/packetsender.go index 0007e8a..6979c15 100644 --- a/packetsender.go +++ b/packetsender.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/google/uuid" "net" "reflect" "strconv" @@ -44,6 +45,12 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro response.Encryption = false response.CompressionThreshold = -2 response.PluginDataSent = map[string]string{} + response.ScanProgress = 0 + response.ServerInfo = ServerInfo{ + Hostname: host, + Port: port, + IP: ip.String(), + } handshakePacketPing, err := createHandshakePacket(-1, host, port, 1, response.CompressionThreshold) if err != nil { @@ -83,6 +90,12 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro _, stringJson := receiveString(packetData) jsonDecoder := json.NewDecoder(strings.NewReader(stringJson)) err = jsonDecoder.Decode(&response) + if err != nil { + errOut = err + println(stringJson) + return + } + response.ScanProgress = 1 if !didRestartConnection { handshakePacketJoin, err := createHandshakePacket(response.Version.Protocol, host, port, 2, response.CompressionThreshold) if err != nil { @@ -116,11 +129,6 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro response.Username = username PlayerUUID = constructOfflinePlayerUUID(username) response.RawMessage = stringJson - if err != nil { - errOut = err - println(stringJson) - return - } state = 1 loginPacket, err := createOfflineLoginPacket(username, response.CompressionThreshold) if err != nil { @@ -161,6 +169,7 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro response.PluginDataSent[channelName] = channelValue } else if state == 2 { response.Encryption = true + response.ScanProgress = 2 return } else if state == 0 { //ping response @@ -578,7 +587,12 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro for i := 0; i < int(numberOfPlayers); i++ { player := PlayerUpdate{} - player.UUID = packetData[currentOffset : currentOffset+16] + newUUID, err := uuid.FromBytes(packetData[currentOffset : currentOffset+16]) + if err != nil { + errOut = err + return + } + player.UUID = newUUID currentOffset += 16 if actions&0x01 != 0x00 { @@ -662,8 +676,9 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro player.DisplayName = &displayName } } - - response.PlayersInfo = append(response.PlayersInfo, player) + if !reflect.DeepEqual(PlayerUUID, player.UUID) { + response.PlayersInfo = append(response.PlayersInfo, player) + } } } break @@ -690,14 +705,14 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro } response.WorldBorder = WorldBorderInfo{ - X: x, - Z: z, - OldDiameter: oldDiameter, - NewDiameter: newDiameter, - Speed: speed, - PortalTeleportBoundry: portalTeleportBoundary, - WarningBlocks: warningBlocks, - WarningTime: warningTime, + X: x, + Z: z, + OldDiameter: oldDiameter, + NewDiameter: newDiameter, + Speed: speed, + PortalTeleportBoundary: portalTeleportBoundary, + WarningBlocks: warningBlocks, + WarningTime: warningTime, } } @@ -752,6 +767,7 @@ func PingIP(ip net.IP, port uint16, host string) (response Response, errOut erro errOut = err return } + response.ScanProgress = 3 return //we dont want chunks } } diff --git a/response.go b/response.go index 264e4af..67aa5b5 100644 --- a/response.go +++ b/response.go @@ -5,11 +5,18 @@ import ( "encoding/json" "errors" "fmt" + "github.com/google/uuid" "io" "regexp" "strings" ) +type ServerInfo struct { + Hostname string `json:"hostname,omitempty"` + Port uint16 `json:"port,omitempty"` + IP string `json:"ip,omitempty"` +} + type PlayerPosition struct { X float64 `json:"x,omitempty"` Y float64 `json:"y,omitempty"` @@ -56,8 +63,13 @@ type PlayerSignatureData struct { PublicKeySignature []byte `json:"publicKeySignature,omitempty"` } +type AddedPlayer struct { + UUID uuid.UUID + PlayerId int +} + type PlayerUpdate struct { - UUID []byte `json:"uuid,omitempty"` + UUID uuid.UUID `json:"uuid,omitempty"` Name string `json:"name,omitempty"` Properties []PlayerProperty `json:"properties,omitempty"` SignatureData *PlayerSignatureData `json:"signatureData,omitempty"` @@ -141,14 +153,14 @@ type TextComponent struct { } type WorldBorderInfo struct { - X float64 `json:"x,omitempty"` - Z float64 `json:"z,omitempty"` - OldDiameter float64 `json:"oldDiameter,omitempty"` - NewDiameter float64 `json:"newDiameter,omitempty"` - Speed int64 `json:"speed,omitempty"` - PortalTeleportBoundry int32 `json:"portalTeleportBoundry,omitempty"` - WarningBlocks int32 `json:"warningBlocks,omitempty"` - WarningTime int32 `json:"warningTime,omitempty"` + X float64 `json:"x,omitempty"` + Z float64 `json:"z,omitempty"` + OldDiameter float64 `json:"oldDiameter,omitempty"` + NewDiameter float64 `json:"newDiameter,omitempty"` + Speed int64 `json:"speed,omitempty"` + PortalTeleportBoundary int32 `json:"portalTeleportBoundry,omitempty"` + WarningBlocks int32 `json:"warningBlocks,omitempty"` + WarningTime int32 `json:"warningTime,omitempty"` } // Custom unmarshaler for the TextComponent type @@ -248,4 +260,6 @@ type Response struct { WorldAge int64 `json:"worldAge,omitempty"` WorldBorder WorldBorderInfo `json:"worldBorder,omitempty"` PlayersInfo []PlayerUpdate `json:"playersinfo,omitempty"` + ServerInfo ServerInfo `json:"ServerInfo,omitempty"` + ScanProgress byte `json:"ScanProgress,omitempty"` } diff --git a/saver.go b/saver.go new file mode 100644 index 0000000..60d3483 --- /dev/null +++ b/saver.go @@ -0,0 +1,693 @@ +package main + +import ( + "database/sql" + "errors" + "github.com/google/uuid" + "log" +) + +func saveResponse(db *sql.DB, response Response) error { + if response.ScanProgress < 1 { + return nil + } + tx, err := db.Begin() + if err != nil { + return err + } + commited := false + defer func(tx *sql.Tx) { + if !commited { + err := tx.Rollback() + if err != nil { + log.Fatalf("Rollback failed: %v", err) + } + } + }(tx) + + // Insert or update server details + serverID, err := insertOrUpdateServer(tx, response.ServerInfo) + if err != nil { + return err + } + + // Insert or update favicon + faviconID, err := insertFavicon(tx, response.Favicon.PngData) + if err != nil { + return err + } + + // Insert a scan entry + scanID, err := insertScan(tx, response, serverID, faviconID) + if err != nil { + return err + } + + var addedPlayers []AddedPlayer + // Insert players and seen players + err = insertPlayersAndSeenPlayers(tx, response.Players.Sample, scanID, &addedPlayers) + if err != nil { + return err + } + + if response.ScanProgress >= 3 { + + err = insertDimensions(tx, response.PlayerLoginInfo.Dimensions, scanID) + if err != nil { + return err + } + + // Insert players and seen players + err = insertPlayersAndSeenPlayersUpdate(tx, response.PlayersInfo, scanID, &addedPlayers) + if err != nil { + return err + } + + // Insert plugin data sent + err = insertPluginData(tx, response.PluginDataSent, scanID) + if err != nil { + return err + } + + // Insert feature flags and link to scan + err = insertFeatureFlags(tx, response.FeatureFlags, scanID) + if err != nil { + return err + } + + // Insert datapacks and link to scan + err = insertDatapacks(tx, response.EnabledDatapacks, scanID) + if err != nil { + return err + } + + // Insert registry entries + err = insertRegistryEntries(tx, response.RegistryDatas, scanID) + if err != nil { + return err + } + + // Insert player updates and properties + err = insertPlayerUpdates(tx, response.PlayersInfo, scanID, &addedPlayers) + if err != nil { + return err + } + + err = saveUpdateTags(tx, response.Tags, scanID) + if err != nil { + return err + } + + } + commited = true + return tx.Commit() +} + +// Insert or update server details +func insertOrUpdateServer(tx *sql.Tx, serverInfo ServerInfo) (int, error) { + // First, try to select the server with the provided hostname, IP, and port + var serverID int + err := tx.QueryRow( + "SELECT id FROM servers WHERE hostname = ? AND ip = ? AND port = ?", + serverInfo.Hostname, serverInfo.IP, serverInfo.Port, + ).Scan(&serverID) + + // If the server already exists, update the details + if err == nil { + _, err := tx.Exec( + "UPDATE servers SET hostname = ?, ip = ?, port = ? WHERE id = ?", + serverInfo.Hostname, serverInfo.IP, serverInfo.Port, serverID, + ) + if err != nil { + return 0, err + } + return serverID, nil + } + + // If the server does not exist, insert a new record + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec( + "INSERT INTO servers (hostname, ip, port) VALUES (?, ?, ?)", + serverInfo.Hostname, serverInfo.IP, serverInfo.Port, + ) + if err != nil { + return 0, err + } + newServerID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(newServerID), nil + } + + // Return error if something went wrong during the SELECT query + return 0, err +} + +// Insert favicon if not exists +func insertFavicon(tx *sql.Tx, pngData []byte) (int, error) { + if pngData == nil { + pngData = []byte{} + } + result, err := tx.Exec("INSERT INTO favicons (favicon) VALUES (?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)", pngData) + if err != nil { + return 0, err + } + faviconID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(faviconID), nil +} + +// Insert scan data +func insertScan(tx *sql.Tx, response Response, serverID int, faviconID int) (int, error) { + result, err := tx.Exec( + `INSERT INTO scans + (server_id, progress, timestamp, favicon_id, raw_message, version, max_players, online_players, description, enforces_secure_chat, + prevents_chat_reports, compression_threshold, is_offline_mode, encryption, message, entity_id, hardcore, view_distance, + simulation_distance, reduced_debug_info, enable_respawn_screen, do_limited_crafting, dimension_type, dimension_name, + hashed_seed, game_mode, previous_game_mode, is_debug, is_flat, has_death_location, death_dimension_name, death_location_x, + death_location_y, death_location_z, portal_cooldown, server_difficulty, + server_difficulty_locked, player_slot, player_location_x, player_location_y,player_location_z,player_location_pitch, + player_location_yaw, invulnerable, flying, allow_flying, creative_mode, flying_speed, field_of_view_modifier, + default_position_spawn_x, default_position_spawn_y, default_position_spawn_z, default_position_spawn_angle, + time_of_day, world_age, world_border_x, world_border_z, world_border_old_diameter, world_border_new_diameter, + world_border_speed, world_border_portal_teleport_boundary, world_border_warning_blocks, world_border_warning_time) + VALUES (?, ?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + serverID, response.ScanProgress, faviconID, response.RawMessage, response.Version.Name, response.Players.Max, response.Players.Online, response.Description.CleanText, + response.EnforcesSecureChat, response.PreventsChatReports, response.CompressionThreshold, response.IsOfflineMode, response.Encryption, + response.Message.CleanText, response.PlayerLoginInfo.EntityID, response.PlayerLoginInfo.Hardcore, response.PlayerLoginInfo.ViewDistance, + response.PlayerLoginInfo.SimulationDistance, response.PlayerLoginInfo.ReducedDebugInfo, response.PlayerLoginInfo.EnableRespawnScreen, + response.PlayerLoginInfo.DoLimitedCrafting, response.PlayerLoginInfo.DimensionType, response.PlayerLoginInfo.DimensionName, + response.PlayerLoginInfo.HashedSeed, response.PlayerLoginInfo.GameMode, response.PlayerLoginInfo.PreviousGameMode, + response.PlayerLoginInfo.IsDebug, response.PlayerLoginInfo.IsFlat, response.PlayerLoginInfo.HasDeathLocation, + response.PlayerLoginInfo.DeathDimensionName, response.PlayerLoginInfo.DeathLocation.X, + response.PlayerLoginInfo.DeathLocation.Y, response.PlayerLoginInfo.DeathLocation.Z, response.PlayerLoginInfo.PortalCooldown, + response.ServerDifficulty.Difficulty, response.ServerDifficulty.Locked, response.PlayerSlot, + response.PlayerLocation.X, response.PlayerLocation.Y, response.PlayerLocation.Z, response.PlayerLocation.Pitch, response.PlayerLocation.Yaw, + response.PlayerAbilities.Invulnerable, response.PlayerAbilities.Flying, response.PlayerAbilities.AllowFlying, response.PlayerAbilities.CreativeMode, response.PlayerAbilities.FlyingSpeed, + response.PlayerAbilities.FieldOfViewModifier, response.DefaultPositionSpawn.Location.X, response.DefaultPositionSpawn.Location.Y, response.DefaultPositionSpawn.Location.Z, response.DefaultPositionSpawn.Angle, + response.TimeOfDay, response.WorldAge, response.WorldBorder.X, response.WorldBorder.Z, response.WorldBorder.OldDiameter, response.WorldBorder.NewDiameter, response.WorldBorder.Speed, response.WorldBorder.PortalTeleportBoundary, + response.WorldBorder.WarningBlocks, response.WorldBorder.WarningTime) + if err != nil { + return 0, err + } + scanID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(scanID), nil +} + +// Insert dimensions and link to scan +func insertDimensions(tx *sql.Tx, dimensions []string, scanID int) error { + for _, dimension := range dimensions { + var dimensionID int + err := tx.QueryRow( + "SELECT id FROM dimensions WHERE name = ?", + dimension, + ).Scan(&dimensionID) + + if errors.Is(err, sql.ErrNoRows) { + // If the dimension does not exist, insert it + result, err := tx.Exec( + "INSERT INTO dimensions (name) VALUES (?)", + dimension, + ) + if err != nil { + return err + } + dimensionID64, err := result.LastInsertId() + if err != nil { + return err + } + dimensionID = int(dimensionID64) + } else if err != nil { + return err + } + + // Link the dimension to the scan + _, err = tx.Exec( + "INSERT INTO dimensions_scans (scan_id, dimension_id) VALUES (?, ?)", + scanID, dimensionID, + ) + if err != nil { + return err + } + } + return nil +} + +// Insert players and seen players +func insertPlayersAndSeenPlayers(tx *sql.Tx, players []Player, scanID int, addedPlayers *[]AddedPlayer) (errOut error) { + if addedPlayers == nil { + return errors.New("added players cannot be nil") + } + for _, player := range players { + // Check if the player has already been added + canAdd := true + for _, addedPlayer := range *addedPlayers { + if addedPlayer.UUID == player.ID { + canAdd = false + break + } + } + if !canAdd { + continue + } + + playerID, err := insertPlayer(tx, player) + if err != nil { + return err + } + *addedPlayers = append(*addedPlayers, AddedPlayer{UUID: player.ID, PlayerId: playerID}) + } + return nil +} + +// Insert players and seen players updates +func insertPlayersAndSeenPlayersUpdate(tx *sql.Tx, players []PlayerUpdate, scanID int, addedPlayers *[]AddedPlayer) (errOut error) { + if addedPlayers == nil { + return errors.New("added players cannot be nil") + } + for _, player := range players { + // Check if the player has already been added + canAdd := true + for _, addedPlayer := range *addedPlayers { + if addedPlayer.UUID == player.UUID { + canAdd = false + break + } + } + if !canAdd { + continue + } + + playerID, err := insertPlayerUpdateIntoPlayers(tx, player) + if err != nil { + return err + } + *addedPlayers = append(*addedPlayers, AddedPlayer{UUID: player.UUID, PlayerId: playerID}) + + } + return nil +} + +// Insert or update player (UUID and username) +func insertPlayer(tx *sql.Tx, player Player) (int, error) { + var playerID int + + // Check if a player with the given UUID already exists + err := tx.QueryRow( + "SELECT id FROM players WHERE uuid = ?", + player.ID.String(), + ).Scan(&playerID) + + if err == nil { + // If the player exists, check if the username needs to be updated + _, err = tx.Exec( + "UPDATE players SET username = ? WHERE id = ? AND username != ?", + player.Name, playerID, player.Name, + ) + if err != nil { + return 0, err + } + return playerID, nil + } + + // If the player does not exist, insert a new record + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec( + "INSERT INTO players (uuid, username) VALUES (?, ?)", + player.ID.String(), player.Name, + ) + if err != nil { + return 0, err + } + newPlayerID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(newPlayerID), nil + } + + // Return error if something went wrong during the SELECT query + return 0, err +} + +// Insert or update player with additional fields (UUID and username) +func insertPlayerUpdateIntoPlayers(tx *sql.Tx, player PlayerUpdate) (int, error) { + var playerID int + + // Check if a player with the given UUID already exists + err := tx.QueryRow( + "SELECT id FROM players WHERE uuid = ?", + player.UUID.String(), + ).Scan(&playerID) + + if err == nil { + // If the player exists, update the relevant fields only if needed + _, err = tx.Exec( + "UPDATE players SET username = ?, game_mode = ?, listed = ?, ping = ?, display_name = ? WHERE id = ? AND (username != ? OR game_mode != ? OR listed != ? OR ping != ? OR display_name != ?)", + player.Name, player.GameMode, player.Listed, player.Ping, player.DisplayName, + playerID, player.Name, player.GameMode, player.Listed, player.Ping, player.DisplayName, + ) + if err != nil { + return 0, err + } + return playerID, nil + } + + // If the player does not exist, insert a new record + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec( + "INSERT INTO players (uuid, username, game_mode, listed, ping, display_name) VALUES (?, ?, ?, ?, ?, ?)", + player.UUID.String(), player.Name, player.GameMode, player.Listed, player.Ping, player.DisplayName, + ) + if err != nil { + return 0, err + } + newPlayerID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(newPlayerID), nil + } + + // Return error if something went wrong during the SELECT query + return 0, err +} + +// Insert plugin data sent +func insertPluginData(tx *sql.Tx, pluginData map[string]string, scanID int) error { + for name, value := range pluginData { + _, err := tx.Exec("INSERT INTO plugin_data_sent (scan_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)", scanID, name, value) + if err != nil { + return err + } + } + return nil +} + +func insertTag(tx *sql.Tx, tagName string) (int, error) { + var tagID int + err := tx.QueryRow("SELECT id FROM tags WHERE name = ?", tagName).Scan(&tagID) + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec("INSERT INTO tags (name) VALUES (?)", tagName) + if err != nil { + return 0, err + } + tagID64, err := result.LastInsertId() + if err != nil { + return 0, err + } + tagID = int(tagID64) + } else if err != nil { + return 0, err + } + return tagID, nil +} + +func insertEntry(tx *sql.Tx, value int32) (int, error) { + var entryID int + err := tx.QueryRow("SELECT id FROM entries WHERE value = ?", value).Scan(&entryID) + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec("INSERT INTO entries (value) VALUES (?)", value) + if err != nil { + return 0, err + } + entryID64, err := result.LastInsertId() + if err != nil { + return 0, err + } + entryID = int(entryID64) + } else if err != nil { + return 0, err + } + return entryID, nil +} + +func saveUpdateTags(tx *sql.Tx, updates []UpdateTag, scanID int) error { + for _, update := range updates { + // Insert the tag registry identifier + registryID, err := insertTagRegistry(tx, update.TagRegistryIdentifier) + if err != nil { + return err + } + + for _, tagArray := range update.Tags { + // Insert the tag + tagID, err := insertTag(tx, tagArray.TagName) + if err != nil { + return err + } + + // Link the tag with the registry + err = insertTagRegistryTag(tx, registryID, tagID) + if err != nil { + return err + } + + // Insert the entries and link them with the tag + for _, entryValue := range tagArray.Entries { + entryID, err := insertEntry(tx, entryValue) + if err != nil { + return err + } + + err = insertTagEntry(tx, tagID, entryID, scanID) + if err != nil { + return err + } + } + } + } + return nil +} + +func insertTagRegistry(tx *sql.Tx, registryIdentifier string) (int, error) { + var registryID int + err := tx.QueryRow("SELECT id FROM tag_registries WHERE registry_identifier = ?", registryIdentifier).Scan(®istryID) + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec("INSERT INTO tag_registries (registry_identifier) VALUES (?)", registryIdentifier) + if err != nil { + return 0, err + } + registryID64, err := result.LastInsertId() + if err != nil { + return 0, err + } + registryID = int(registryID64) + } else if err != nil { + return 0, err + } + return registryID, nil +} + +func insertTagRegistryTag(tx *sql.Tx, registryID int, tagID int) error { + _, err := tx.Exec("INSERT IGNORE INTO tag_registry_tags (registry_id, tag_id) VALUES (?, ?)", registryID, tagID) + return err +} + +func insertTagEntry(tx *sql.Tx, tagID int, entryID int, scanID int) error { + _, err := tx.Exec("INSERT IGNORE INTO tag_entries (tag_id, entry_id, scan_id) VALUES (?, ?, ?)", tagID, entryID, scanID) + return err +} + +// Insert feature flags and link to scan +func insertFeatureFlags(tx *sql.Tx, featureFlags []string, scanID int) error { + for _, flagText := range featureFlags { + var featureID int + err := tx.QueryRow( + "SELECT id FROM feature_flags WHERE flag_text = ?", + flagText, + ).Scan(&featureID) + + if errors.Is(err, sql.ErrNoRows) { + // If the feature flag does not exist, insert it + featureID, err = insertFeatureFlag(tx, flagText) + if err != nil { + return err + } + } else if err != nil { + return err + } + + // Link the feature flag to the scan + _, err = tx.Exec( + "INSERT INTO feature_flag_scans (feature_flag_id, scan_id) VALUES (?, ?)", + featureID, scanID, + ) + if err != nil { + return err + } + } + return nil +} + +// Insert feature flag (ID, flagText) +func insertFeatureFlag(tx *sql.Tx, flagText string) (int, error) { + result, err := tx.Exec("INSERT INTO feature_flags (flag_text) VALUES (?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)", flagText) + if err != nil { + return 0, err + } + featureID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(featureID), nil +} + +// Insert datapacks and link to scan +func insertDatapacks(tx *sql.Tx, datapacks []DatapackInfo, scanID int) error { + for _, dp := range datapacks { + datapackID, err := insertDatapack(tx, dp) + if err != nil { + return err + } + _, err = tx.Exec("INSERT INTO datapack_scans (datapack_id, scan_id) VALUES (?, ?)", datapackID, scanID) + if err != nil { + return err + } + } + return nil +} + +// Insert or update datapack (ID, namespace, datapack ID, version) +func insertDatapack(tx *sql.Tx, dp DatapackInfo) (int, error) { + // First, try to select the datapack with the provided namespace and datapack ID + var datapackID int + err := tx.QueryRow( + "SELECT id FROM datapacks WHERE namespace = ? AND datapack_id = ?", + dp.Namespace, dp.ID, + ).Scan(&datapackID) + + // If the datapack already exists, update the version + if err == nil { + _, err = tx.Exec( + "UPDATE datapacks SET version = ? WHERE id = ?", + dp.Version, datapackID, + ) + if err != nil { + return 0, err + } + return datapackID, nil + } + + // If the datapack does not exist, insert a new record + if errors.Is(err, sql.ErrNoRows) { + result, err := tx.Exec( + "INSERT INTO datapacks (namespace, datapack_id, version) VALUES (?, ?, ?)", + dp.Namespace, dp.ID, dp.Version, + ) + if err != nil { + return 0, err + } + newDatapackID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(newDatapackID), nil + } + + // Return error if something went wrong during the SELECT query + return 0, err +} + +// Insert registry entries +func insertRegistryEntries(tx *sql.Tx, registryDatas []RegistryData, scanID int) error { + for _, entry1 := range registryDatas { + for _, entry := range entry1.Entries { + _, err := insertRegistryEntry(tx, entry, entry1.RegistryID, scanID) + if err != nil { + return err + } + // Assuming linking logic with scan ID is required + } + } + return nil +} + +// Insert registry entry (ID, hasNBT, nbt_data as BLOB) +func insertRegistryEntry(tx *sql.Tx, entry RegistryEntry, registryID string, scanID int) (int, error) { + var nbtData []byte + if entry.HasNBT { + nbtData = entry.NBTData + } else { + nbtData = []byte{} + } + result, err := tx.Exec("INSERT IGNORE INTO registry_entries (scan_id, registry_id, entry_id, has_nbt, nbt_data) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE scan_id = LAST_INSERT_ID(scan_id)", scanID, registryID, entry.EntryID, entry.HasNBT, nbtData) + if err != nil { + return 0, err + } + entryID, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(entryID), nil +} + +// Insert player updates and properties +func insertPlayerUpdates(tx *sql.Tx, playerUpdates []PlayerUpdate, scanID int, addedPlayers *[]AddedPlayer) error { + for _, update := range playerUpdates { + updateID, err := insertPlayerUpdate(tx, update, scanID, addedPlayers) + if err != nil { + return err + } + err = insertPlayerProperties(tx, update.Properties, updateID) + if err != nil { + return err + } + } + return nil +} + +// Insert player update (linked to player and scan) +func insertPlayerUpdate(tx *sql.Tx, update PlayerUpdate, scanID int, addedPlayers *[]AddedPlayer) (int, error) { + playerID, err := getPlayerID(tx, update.UUID, addedPlayers) + if err != nil { + return 0, err + } + + // If the update exists, update the existing record + + res, err := tx.Exec( + "INSERT INTO player_sightnings (player_id, username, game_mode, listed, ping, display_name, scan_id) VALUES (?, ?, ?, ?, ?, ?, ?)", + playerID, update.Name, update.GameMode, update.Listed, update.Ping, update.DisplayName, scanID, + ) + if err != nil { + return 0, err + } + lastID, err := res.LastInsertId() + if err != nil { + return 0, err + } + return int(lastID), nil +} + +// Insert player properties (linked to player update) +func insertPlayerProperties(tx *sql.Tx, properties []PlayerProperty, updateID int) error { + for _, prop := range properties { + _, err := tx.Exec("INSERT INTO player_properties (player_update_id, name, value, signature) VALUES (?, ?, ?, ?)", updateID, prop.Name, prop.Value, prop.Signature) + if err != nil { + return err + } + } + return nil +} + +// Helper function to get player ID by UUID +func getPlayerID(tx *sql.Tx, uuid uuid.UUID, addedPlayers *[]AddedPlayer) (int, error) { + for _, addedPlayer := range *addedPlayers { + if addedPlayer.UUID == uuid { + return addedPlayer.PlayerId, nil + } + } + var playerID int + err := tx.QueryRow("SELECT id FROM players WHERE uuid = ?", uuid.String()).Scan(&playerID) + if err != nil { + return 0, err + } + return playerID, nil +}