feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,231 @@
use crate::State;
use crate::util::fetch::INSECURE_REQWEST_CLIENT;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const CONNECTOR_URL_ENV: &str = "AXOLOTL_PLANET_MINECRAFT_CONNECTOR_URL";
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanetMinecraftProject {
pub id: String,
pub title: String,
pub page_url: String,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub versions: Vec<PlanetMinecraftVersion>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanetMinecraftVersion {
pub id: String,
pub name: String,
pub game_versions: Vec<String>,
pub download: PlanetMinecraftDownload,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanetMinecraftDownload {
pub page_url: String,
#[serde(default)]
pub file_name: Option<String>,
#[serde(default)]
pub direct_url: Option<String>,
#[serde(default)]
pub sha256: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum PlanetMinecraftInstallRoute {
Automatic {
direct_url: String,
sha256: String,
file_name: Option<String>,
},
Manual {
page_url: String,
file_name: Option<String>,
},
}
impl PlanetMinecraftDownload {
pub fn install_route(&self) -> PlanetMinecraftInstallRoute {
match (
self.direct_url.as_deref().filter(|url| {
!url.trim().is_empty() && validated_download_url(url).is_ok()
}),
self.sha256
.as_deref()
.filter(|hash| !hash.trim().is_empty()),
) {
(Some(direct_url), Some(sha256)) => {
PlanetMinecraftInstallRoute::Automatic {
direct_url: direct_url.to_string(),
sha256: sha256.to_string(),
file_name: self.file_name.clone(),
}
}
_ => PlanetMinecraftInstallRoute::Manual {
page_url: self.page_url.clone(),
file_name: self.file_name.clone(),
},
}
}
}
pub fn connector_base_url() -> crate::Result<String> {
let value = std::env::var(CONNECTOR_URL_ENV).map_err(|_| {
crate::ErrorKind::InputError(
"Planet Minecraft connector is not configured".to_string(),
)
})?;
let url = reqwest::Url::parse(&value).map_err(|_| {
crate::ErrorKind::InputError(
"Planet Minecraft connector URL is invalid".to_string(),
)
})?;
if url.scheme() != "https" {
return Err(crate::ErrorKind::InputError(
"Planet Minecraft connector must use HTTPS".to_string(),
)
.into());
}
Ok(value.trim_end_matches('/').to_string())
}
pub async fn search_projects(
query: &str,
game_version: Option<&str>,
) -> crate::Result<Vec<PlanetMinecraftProject>> {
let mut url = format!(
"{}/projects?query={}",
connector_base_url()?,
urlencoding::encode(query)
);
if let Some(game_version) = game_version.filter(|value| !value.is_empty()) {
url.push_str(&format!(
"&game_version={}",
urlencoding::encode(game_version)
));
}
connector_get(&url).await
}
pub async fn get_project(id: &str) -> crate::Result<PlanetMinecraftProject> {
connector_get(&format!(
"{}/projects/{}",
connector_base_url()?,
urlencoding::encode(id)
))
.await
}
pub async fn download_verified_file(
direct_url: &str,
expected_sha256: &str,
) -> crate::Result<Bytes> {
let url = validated_download_url(direct_url)?;
if expected_sha256.trim().is_empty() {
return Err(crate::ErrorKind::InputError(
"Planet Minecraft downloads require a SHA-256".to_string(),
)
.into());
}
let state = State::get().await?;
let _permit = state.api_semaphore.0.acquire().await?;
let bytes = INSECURE_REQWEST_CLIENT
.get(url)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
if bytes.is_empty() {
return Err(crate::ErrorKind::InputError(
"Planet Minecraft returned an empty file".to_string(),
)
.into());
}
let actual = format!("{:x}", Sha256::digest(&bytes));
if !actual.eq_ignore_ascii_case(expected_sha256.trim()) {
return Err(crate::ErrorKind::OtherError(
"Planet Minecraft SHA-256 mismatch".to_string(),
)
.into());
}
Ok(bytes)
}
fn validated_download_url(value: &str) -> crate::Result<reqwest::Url> {
let url = reqwest::Url::parse(value).map_err(|_| {
crate::ErrorKind::InputError(
"Planet Minecraft download URL is invalid".to_string(),
)
})?;
if url.scheme() != "https" {
return Err(crate::ErrorKind::InputError(
"Planet Minecraft download URL must use HTTPS".to_string(),
)
.into());
}
Ok(url)
}
async fn connector_get<T>(url: &str) -> crate::Result<T>
where
T: for<'de> Deserialize<'de>,
{
let state = State::get().await?;
let _permit = state.api_semaphore.0.acquire().await?;
Ok(INSECURE_REQWEST_CLIENT
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn downloads_without_a_hash_remain_manual() {
let download = PlanetMinecraftDownload {
page_url: "https://www.planetminecraft.com/mod/example".to_string(),
file_name: Some("example.jar".to_string()),
direct_url: Some("https://host.example/example.jar".to_string()),
sha256: None,
};
assert!(matches!(
download.install_route(),
PlanetMinecraftInstallRoute::Manual { .. }
));
}
#[test]
fn direct_downloads_require_https() {
assert!(validated_download_url("http://example.com/file.jar").is_err());
assert!(validated_download_url("https://example.com/file.jar").is_ok());
}
#[test]
fn non_https_direct_downloads_fall_back_to_manual_import() {
let download = PlanetMinecraftDownload {
page_url: "https://www.planetminecraft.com/mod/example".to_string(),
file_name: Some("example.jar".to_string()),
direct_url: Some("http://host.example/example.jar".to_string()),
sha256: Some("abc".to_string()),
};
assert!(matches!(
download.install_route(),
PlanetMinecraftInstallRoute::Manual { .. }
));
}
}