Files
AstralRinth/apps/labrinth/src/models/exp/minecraft.rs
T
aecsocket d14360aba5 Unify server pinging implementations between app and backend (#5510)
* Improve ping impl to bring parity to app lib impl

* Fix issue with new impl

* fix labrinth compile

* wip: why do servers not provide server info..

* Fix ping impl overriding port

* fix theseus_gui

* remove unneeded recursion lmit
2026-03-13 16:21:09 +00:00

445 lines
12 KiB
Rust

use std::time::Duration;
use chrono::{DateTime, Utc};
use eyre::Result;
use serde::{Deserialize, Serialize};
use tracing::warn;
use validator::Validate;
use crate::{
models::{
exp::{
ProjectComponentKind,
component::{self, Component, ComponentEdit, ComponentQuery},
project::{
ProjectComponent, ProjectQueryContext, ProjectQueryRequirements,
},
},
ids::{ProjectId, VersionId},
},
util::error::Context,
};
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum Language {
En,
Es,
Pt,
Fr,
De,
It,
Nl,
Ru,
Uk,
Pl,
Cs,
Sk,
Hu,
Ro,
Bg,
Hr,
Sr,
El,
Tr,
Ar,
He,
Hi,
Bn,
Ur,
Zh,
Ja,
Ko,
Th,
Vi,
Id,
Ms,
Tl,
Sv,
No,
Da,
Fi,
Lt,
Lv,
Et,
Af,
Am,
Az,
Be,
Bs,
Ca,
Eo,
Eu,
Fa,
Ga,
Gl,
Hy,
Is,
Ka,
Kk,
Km,
Kn,
Lo,
Mk,
Ml,
Mn,
Mr,
My,
Ne,
Pa,
Si,
Sl,
Sq,
Sw,
Ta,
Te,
Uz,
Yo,
Zu,
}
component::define! {
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct ModProject {}
/// Listing for a Minecraft server.
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct ServerProject {
#[base(serde(default))]
#[edit(serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
))]
#[create(optional)]
/// Maximum number of players allowed on the server.
pub max_players: Option<u32>,
#[base(serde(default))]
#[edit(serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
))]
#[create(optional)]
/// Country which this server is hosted in.
#[validate(length(min = 2, max = 2))]
pub country: Option<String>,
#[base(serde(default))]
#[edit(serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
))]
#[create(optional)]
/// Geographical region which this server is hosted in.
pub region: Option<ServerRegion>,
#[base(serde(default))]
#[edit(serde(default))]
#[create(default)]
/// Languages which the owners of this server prefer.
pub languages: Vec<Language>,
#[base(serde(default))]
#[edit(serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
))]
#[create(optional)]
/// Which version of the listing this server is currently using.
pub active_version: Option<VersionId>,
}
/// Version of a Minecraft Java server listing.
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct JavaServerVersion {}
/// Listing for a Minecraft Bedrock server.
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct BedrockServerProject {
#[base()]
#[edit(serde(default))]
#[create(required)]
/// Address (IP or domain name) of the Bedrock server, excluding port.
#[validate(length(max = 255))]
pub address: String,
}
}
impl ProjectComponent for ModProject {
fn kind() -> ProjectComponentKind {
ProjectComponentKind::MinecraftMod
}
}
impl ProjectComponent for ServerProject {
fn kind() -> ProjectComponentKind {
ProjectComponentKind::MinecraftServer
}
}
impl ProjectComponent for JavaServerProject {
fn kind() -> ProjectComponentKind {
ProjectComponentKind::MinecraftJavaServer
}
}
impl ProjectComponent for BedrockServerProject {
fn kind() -> ProjectComponentKind {
ProjectComponentKind::MinecraftBedrockServer
}
}
/// Listing for a Minecraft Java server.
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct JavaServerProject {
/// Address (IP or domain name) of the Java server, excluding port.
#[validate(length(max = 255))]
pub address: String,
/// What game content this server is using.
#[serde(default)]
pub content: ServerContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct JavaServerProjectEdit {
#[validate(length(max = 255))]
#[serde(default)]
pub address: Option<String>,
#[serde(default)]
pub content: Option<ServerContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct JavaServerProjectQuery {
pub address: String,
pub content: ServerContentQuery,
pub ping: Option<JavaServerPing>,
pub verified_plays_2w: Option<u64>,
pub verified_plays_4w: Option<u64>,
}
impl Component for JavaServerProject {
type EntityId = ProjectId;
type Query = JavaServerProjectQuery;
type Edit = JavaServerProjectEdit;
}
impl ComponentQuery for JavaServerProjectQuery {
type Component = JavaServerProject;
type Requirements = ProjectQueryRequirements;
type Context = ProjectQueryContext;
fn collect_requirements(
serial: &Self::Component,
project_id: ProjectId,
requirements: &mut ProjectQueryRequirements,
) {
match serial.content {
ServerContent::Vanilla { .. } => {}
ServerContent::Modpack { version_id } => {
requirements.partial_versions.insert(version_id);
}
}
requirements.minecraft_java_server_pings.insert(project_id);
requirements.minecraft_server_analytics.insert(project_id);
}
fn populate(
serial: Self::Component,
project_id: ProjectId,
context: &ProjectQueryContext,
) -> Result<Self> {
let analytics = context.minecraft_server_analytics.get(&project_id);
Ok(Self {
address: serial.address,
content: match serial.content {
ServerContent::Vanilla {
supported_game_versions,
recommended_game_version,
} => ServerContentQuery::Vanilla {
supported_game_versions,
recommended_game_version,
},
ServerContent::Modpack { version_id } => {
match context.partial_versions.get(&version_id) {
Some(version) => ServerContentQuery::Modpack {
version_id,
project_id: version.project_id,
project_name: version.project_name.clone(),
project_icon: version.project_icon.clone(),
},
None => {
// TODO: should be upgraded to an error,
// but it's too easy to fall into this illegal state right now
warn!("no modpack info for version {version_id:?}");
ServerContentQuery::Modpack {
version_id,
project_id: ProjectId(0),
project_name: String::new(),
project_icon: String::new(),
}
}
}
}
},
ping: context
.minecraft_java_server_pings
.get(&project_id)
.cloned(),
verified_plays_2w: analytics.map(|a| a.verified_plays_2w),
verified_plays_4w: analytics.map(|a| a.verified_plays_4w),
})
}
}
impl ComponentEdit for JavaServerProjectEdit {
type Component = JavaServerProject;
fn create(self) -> Result<Self::Component> {
Ok(JavaServerProject {
address: self.address.wrap_err("missing `address`")?,
content: self.content.unwrap_or_default(),
})
}
async fn apply_to(self, component: &mut Self::Component) -> Result<()> {
if let Some(address) = self.address {
component.address = address;
}
if let Some(content) = self.content {
component.content = content;
}
Ok(())
}
}
/// What game content a [`JavaServerProject`] is using.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ServerContent {
/// Server runs modded content with a modpack found on the Modrinth platform.
Modpack {
/// Version ID of the modpack which the server runs.
///
/// This version may or may not belong to the server project, since
/// server projects may also be treated as modpacks.
version_id: VersionId,
},
/// Server is a vanilla Minecraft server.
Vanilla {
/// List of supported Minecraft Java client versions which can join this
/// server.
supported_game_versions: Vec<String>,
/// Recommended Minecraft Java client version to use to join this server.
recommended_game_version: Option<String>,
},
}
/// What game content a [`JavaServerProject`] is using.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ServerContentQuery {
/// Server runs modded content with a modpack found on the Modrinth platform.
Modpack {
version_id: VersionId,
project_id: ProjectId,
project_name: String,
project_icon: String,
},
/// Server is a vanilla Minecraft server.
Vanilla {
/// List of supported Minecraft Java client versions which can join this
/// server.
supported_game_versions: Vec<String>,
/// Recommended Minecraft Java client version to use to join this server.
recommended_game_version: Option<String>,
},
}
impl Default for ServerContent {
fn default() -> Self {
ServerContent::Vanilla {
supported_game_versions: Vec::new(),
recommended_game_version: None,
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ServerRegion {
UsEast,
UsWest,
Europe,
Asia,
Australia,
SouthAmerica,
MiddleEast,
Russia,
}
/// Recorded ping attempt that Labrinth made to a Minecraft Java server project.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct JavaServerPing {
/// When the ping was performed.
pub when: DateTime<Utc>,
/// Address of the server at the time of the ping.
pub address: String,
/// If the ping was successful, info on the ping response.
pub data: Option<JavaServerPingData>,
}
/// Ping response data for a Minecraft Java server.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct JavaServerPingData {
/// How long it took for the Labrinth worker to ping the server.
///
/// Note: this is explicitly *not* a client-side ping time, so this should
/// not be used to display to a client how much latency they have to a
/// specific server. This is purely for internal metrics.
pub latency: Duration,
/// Reported version name of the server.
pub version_name: String,
/// Reported version protocol number of the server.
pub version_protocol: i32,
/// Description/MOTD of the server as shown in the server list.
pub description: Option<serde_json::Value>,
/// Number of players online at the time.
pub players_online: Option<i32>,
/// Maximum number of players allowed on the server.
pub players_max: Option<i32>,
}
component::relations! {
pub(super) static PROJECT_COMPONENT_RELATIONS: ProjectComponentKind = {
use ProjectComponentKind::*;
[
[MinecraftMod].only(),
[MinecraftServer, MinecraftJavaServer, MinecraftBedrockServer].only(),
MinecraftJavaServer.requires(MinecraftServer),
MinecraftBedrockServer.requires(MinecraftServer),
]
}
}