minerecon/saver.go
2024-08-23 12:19:44 +02:00

694 lines
20 KiB
Go

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(&registryID)
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
}