From eb51afaea850d7d0d8da03473d10afad90483d32 Mon Sep 17 00:00:00 2001 From: Rbanh Date: Sat, 29 Mar 2025 01:00:49 -0400 Subject: [PATCH] Add web crawler infrastructure and HangarMC implementation --- ROADMAP.md | 8 +- src-tauri/Cargo.lock | 366 +++++++++++++-- src-tauri/Cargo.toml | 1 + src-tauri/src/crawlers/hangar.rs | 203 ++++++++ src-tauri/src/crawlers/mod.rs | 4 + src-tauri/src/lib.rs | 153 ++++++- src-tauri/src/lib.rs.bak | 763 +++++++++++++++++++++++++++++++ 7 files changed, 1466 insertions(+), 32 deletions(-) create mode 100644 src-tauri/src/crawlers/hangar.rs create mode 100644 src-tauri/src/crawlers/mod.rs create mode 100644 src-tauri/src/lib.rs.bak diff --git a/ROADMAP.md b/ROADMAP.md index 4d245db..20035ea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ - [x] Create project roadmap - [x] Initialize Tauri + React project - [x] Setup basic project structure -- [ ] Create GitHub repository (optional) +- [x] Create Remote repository (optional) ## Core Infrastructure (In Progress) - [x] Setup SQLite or JSON storage for plugin data @@ -18,9 +18,9 @@ - [x] Design plugin metadata extraction system - [x] Build plugin hash identification system -## Web Crawler Development (Upcoming) -- [ ] Create base web crawler architecture -- [ ] Implement HangarMC crawler +## Web Crawler Development (In Progress) +- [x] Create base web crawler architecture +- [x] Implement HangarMC crawler - [ ] Implement SpigotMC crawler - [ ] Implement Modrinth crawler - [ ] Implement GitHub releases crawler diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5b24a71..7f3ee63 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -606,6 +606,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -629,9 +639,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -642,7 +652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.10.0", "libc", ] @@ -915,7 +925,7 @@ dependencies = [ "rustc_version", "toml", "vswhom", - "winreg", + "winreg 0.52.0", ] [[package]] @@ -924,6 +934,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -1039,6 +1058,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1046,7 +1074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1060,6 +1088,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1476,6 +1510,25 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.8.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1535,6 +1588,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.15", +] + [[package]] name = "http" version = "1.3.1" @@ -1546,6 +1610,17 @@ dependencies = [ "itoa 1.0.15", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1553,7 +1628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -1564,8 +1639,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -1575,6 +1650,36 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.15", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -1584,8 +1689,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "httparse", "itoa 1.0.15", "pin-project-lite", @@ -1594,6 +1699,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -1603,9 +1721,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", "pin-project-lite", "socket2", "tokio", @@ -2164,6 +2282,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2494,6 +2629,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2780,6 +2959,7 @@ name = "plugsnatcher" version = "0.1.0" dependencies = [ "regex", + "reqwest 0.11.27", "serde", "serde_json", "sha2", @@ -3098,6 +3278,46 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + [[package]] name = "reqwest" version = "0.12.15" @@ -3108,10 +3328,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "ipnet", "js-sys", @@ -3123,7 +3343,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tokio-util", "tower", @@ -3202,6 +3422,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -3223,6 +3452,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3256,6 +3494,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -3522,7 +3783,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "objc2 0.5.2", @@ -3643,6 +3904,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3663,6 +3930,27 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3683,7 +3971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", "dispatch", @@ -3748,7 +4036,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http", + "http 1.3.1", "jni", "libc", "log", @@ -3760,7 +4048,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.12.15", "serde", "serde_json", "serde_repr", @@ -3934,7 +4222,7 @@ dependencies = [ "cookie", "dpi", "gtk", - "http", + "http 1.3.1", "jni", "raw-window-handle", "serde", @@ -3952,7 +4240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "087188020fd6facb8578fe9b38e81fa0fe5fb85744c73da51a299f94a530a1e3" dependencies = [ "gtk", - "http", + "http 1.3.1", "jni", "log", "objc2 0.6.0", @@ -3985,7 +4273,7 @@ dependencies = [ "dunce", "glob", "html5ever", - "http", + "http 1.3.1", "infer", "json-patch", "kuchikiki", @@ -4148,6 +4436,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.14" @@ -4226,7 +4524,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -4431,6 +4729,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -5149,6 +5453,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.52.0" @@ -5195,7 +5509,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http", + "http 1.3.1", "javascriptcore-rs", "jni", "kuchikiki", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5b7097b..00630ae 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,4 +28,5 @@ yaml-rust = "0.4" walkdir = "2.4" regex = "1.10" sha2 = "0.10" +reqwest = { version = "0.11", features = ["blocking", "json"] } diff --git a/src-tauri/src/crawlers/hangar.rs b/src-tauri/src/crawlers/hangar.rs new file mode 100644 index 0000000..66f6c3d --- /dev/null +++ b/src-tauri/src/crawlers/hangar.rs @@ -0,0 +1,203 @@ +use std::error::Error; +use std::path::Path; +use serde::{Serialize, Deserialize}; +use crate::{HttpClient, RepositoryCrawler, RepositoryPlugin, RepositorySource}; + +// HangarMC API response structures +#[derive(Debug, Serialize, Deserialize)] +struct HangarProjectsResponse { + pagination: HangarPagination, + result: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct HangarPagination { + limit: u32, + offset: u32, + count: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +struct HangarProject { + id: String, + name: String, + namespace: HangarNamespace, + category: String, + description: Option, + stats: HangarStats, + last_updated: String, + icon_url: Option, + created_at: String, + visibility: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct HangarNamespace { + owner: String, + slug: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct HangarStats { + views: u64, + downloads: u64, + recent_views: u64, + recent_downloads: u64, + stars: u64, + watchers: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +struct HangarVersionsResponse { + pagination: HangarPagination, + result: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct HangarVersion { + name: String, + created_at: String, + description: Option, + downloads: u64, + file_size: u64, + platform_versions: Vec, +} + +// HangarMC crawler implementation +pub struct HangarCrawler { + client: HttpClient, + api_base_url: String, +} + +impl HangarCrawler { + pub fn new() -> Self { + HangarCrawler { + client: HttpClient::new(), + api_base_url: "https://hangar.papermc.io/api/v1".to_string(), + } + } + + fn get_project_details(&self, owner: &str, slug: &str) -> Result> { + let url = format!("{}/projects/{}/{}", self.api_base_url, owner, slug); + let response = self.client.get(&url)?; + let project: HangarProject = serde_json::from_str(&response)?; + Ok(project) + } + + fn get_project_versions(&self, owner: &str, slug: &str) -> Result, Box> { + let url = format!("{}/projects/{}/{}/versions", self.api_base_url, owner, slug); + let response = self.client.get(&url)?; + let versions_response: HangarVersionsResponse = serde_json::from_str(&response)?; + Ok(versions_response.result) + } + + fn build_download_url(&self, owner: &str, slug: &str, version: &str) -> String { + format!("https://hangar.papermc.io/api/v1/projects/{}/{}/versions/{}/download", owner, slug, version) + } +} + +impl RepositoryCrawler for HangarCrawler { + fn search(&self, query: &str) -> Result, Box> { + let url = format!("{}/projects?query={}&limit=20", self.api_base_url, query); + let response = self.client.get(&url)?; + + let projects_response: HangarProjectsResponse = serde_json::from_str(&response)?; + let mut results = Vec::new(); + + for project in projects_response.result { + // For each project, get the latest version + let versions = self.get_project_versions(&project.namespace.owner, &project.namespace.slug)?; + + if let Some(latest_version) = versions.first() { + results.push(RepositoryPlugin { + id: format!("{}/{}", project.namespace.owner, project.namespace.slug), + name: project.name, + version: latest_version.name.clone(), + description: project.description, + authors: vec![project.namespace.owner.clone()], + download_url: self.build_download_url(&project.namespace.owner, &project.namespace.slug, &latest_version.name), + repository: RepositorySource::HangarMC, + page_url: format!("https://hangar.papermc.io/{}/{}", project.namespace.owner, project.namespace.slug), + download_count: Some(project.stats.downloads), + last_updated: Some(project.last_updated), + icon_url: project.icon_url, + minecraft_versions: latest_version.platform_versions.clone(), + categories: vec![project.category], + rating: None, // HangarMC uses stars, not ratings + file_size: Some(latest_version.file_size), + file_hash: None, // HangarMC API doesn't provide file hashes + }); + } + } + + Ok(results) + } + + fn get_plugin_details(&self, plugin_id: &str) -> Result> { + let parts: Vec<&str> = plugin_id.split('/').collect(); + if parts.len() != 2 { + return Err("Invalid plugin ID format for HangarMC. Expected 'owner/slug'".into()); + } + + let owner = parts[0]; + let slug = parts[1]; + + let project = self.get_project_details(owner, slug)?; + let versions = self.get_project_versions(owner, slug)?; + + if let Some(latest_version) = versions.first() { + Ok(RepositoryPlugin { + id: plugin_id.to_string(), + name: project.name, + version: latest_version.name.clone(), + description: project.description, + authors: vec![project.namespace.owner.clone()], + download_url: self.build_download_url(owner, slug, &latest_version.name), + repository: RepositorySource::HangarMC, + page_url: format!("https://hangar.papermc.io/{}/{}", owner, slug), + download_count: Some(project.stats.downloads), + last_updated: Some(project.last_updated), + icon_url: project.icon_url, + minecraft_versions: latest_version.platform_versions.clone(), + categories: vec![project.category], + rating: None, + file_size: Some(latest_version.file_size), + file_hash: None, + }) + } else { + Err("No versions found for this plugin".into()) + } + } + + fn get_plugin_versions(&self, plugin_id: &str) -> Result, Box> { + let parts: Vec<&str> = plugin_id.split('/').collect(); + if parts.len() != 2 { + return Err("Invalid plugin ID format for HangarMC. Expected 'owner/slug'".into()); + } + + let owner = parts[0]; + let slug = parts[1]; + + let versions = self.get_project_versions(owner, slug)?; + Ok(versions.into_iter().map(|v| v.name).collect()) + } + + fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result> { + let parts: Vec<&str> = plugin_id.split('/').collect(); + if parts.len() != 2 { + return Err("Invalid plugin ID format for HangarMC. Expected 'owner/slug'".into()); + } + + let owner = parts[0]; + let slug = parts[1]; + + let download_url = self.build_download_url(owner, slug, version); + self.client.download(&download_url, destination)?; + + Ok(destination.to_string_lossy().to_string()) + } + + fn get_repository_name(&self) -> RepositorySource { + RepositorySource::HangarMC + } +} \ No newline at end of file diff --git a/src-tauri/src/crawlers/mod.rs b/src-tauri/src/crawlers/mod.rs new file mode 100644 index 0000000..c1b96bc --- /dev/null +++ b/src-tauri/src/crawlers/mod.rs @@ -0,0 +1,4 @@ +pub mod hangar; + +// Re-export the crawler implementations +pub use hangar::HangarCrawler; \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index da74622..e190bad 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,6 +8,12 @@ use zip::ZipArchive; use yaml_rust::{YamlLoader, Yaml}; use std::fs::File; use sha2::{Sha256, Digest}; +use reqwest; +use std::error::Error; + +// Add the crawlers module +mod crawlers; +use crawlers::HangarCrawler; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum ServerType { @@ -603,12 +609,155 @@ fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } +// Web Crawler Module +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum RepositorySource { + HangarMC, + SpigotMC, + Modrinth, + GitHub, + BukkitDev, + Custom(String), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RepositoryPlugin { + id: String, // Unique identifier in the repository + name: String, // Plugin name + version: String, // Latest version + description: Option, + authors: Vec, + download_url: String, // URL to download the plugin + repository: RepositorySource, + page_url: String, // URL to the plugin page + download_count: Option, + last_updated: Option, + icon_url: Option, + minecraft_versions: Vec, + categories: Vec, + rating: Option, + file_size: Option, + file_hash: Option, +} + +// Trait for implementing different repository crawlers +pub trait RepositoryCrawler { + fn search(&self, query: &str) -> Result, Box>; + fn get_plugin_details(&self, plugin_id: &str) -> Result>; + fn get_plugin_versions(&self, plugin_id: &str) -> Result, Box>; + fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result>; + fn get_repository_name(&self) -> RepositorySource; +} + +// Basic HTTP client for crawler implementations +pub struct HttpClient { + client: reqwest::blocking::Client, +} + +impl HttpClient { + pub fn new() -> Self { + let client = reqwest::blocking::Client::builder() + .user_agent("PlugSnatcher/0.1.0") + .build() + .unwrap_or_else(|_| reqwest::blocking::Client::new()); + + HttpClient { client } + } + + pub fn get(&self, url: &str) -> Result> { + let response = self.client.get(url).send()?; + + if response.status().is_success() { + Ok(response.text()?) + } else { + Err(format!("HTTP error: {}", response.status()).into()) + } + } + + pub fn download(&self, url: &str, destination: &Path) -> Result<(), Box> { + let response = self.client.get(url).send()?; + + if response.status().is_success() { + let bytes = response.bytes()?; + fs::write(destination, bytes)?; + Ok(()) + } else { + Err(format!("Download failed: {}", response.status()).into()) + } + } +} + +// Helper function to get crawler for a specific repository +fn get_crawler(repository: &RepositorySource) -> Option> { + match repository { + RepositorySource::HangarMC => Some(Box::new(HangarCrawler::new())), + // Other repositories will be implemented later + _ => None, + } +} + +// Command to search for plugins in specified repositories +#[command] +pub fn search_repository_plugins(query: &str, repositories: Vec) -> Result, String> { + let mut results: Vec = Vec::new(); + + // Try each requested repository + for repo in repositories { + if let Some(crawler) = get_crawler(&repo) { + match crawler.search(query) { + Ok(repo_results) => { + results.extend(repo_results); + }, + Err(e) => { + println!("Error searching in repository {:?}: {}", repo, e); + // Continue with other repositories even if one fails + } + } + } else { + println!("Repository crawler for {:?} not implemented yet", repo); + } + } + + if results.is_empty() { + Err("No plugins found or repositories not implemented yet".to_string()) + } else { + Ok(results) + } +} + +// Command to get plugin details from a specific repository +#[command] +pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySource) -> Result { + if let Some(crawler) = get_crawler(&repository) { + crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string()) + } else { + Err(format!("Repository crawler for {:?} not implemented yet", repository)) + } +} + +// Command to download a plugin from a repository +#[command] +pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result { + if let Some(crawler) = get_crawler(&repository) { + crawler + .download_plugin(plugin_id, version, Path::new(destination)) + .map_err(|e| e.to_string()) + } else { + Err(format!("Repository crawler for {:?} not implemented yet", repository)) + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) - .invoke_handler(tauri::generate_handler![greet, scan_server_directory]) + .invoke_handler(tauri::generate_handler![ + greet, + scan_server_directory, + search_repository_plugins, + get_repository_plugin_details, + download_repository_plugin + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/lib.rs.bak b/src-tauri/src/lib.rs.bak new file mode 100644 index 0000000..e190bad --- /dev/null +++ b/src-tauri/src/lib.rs.bak @@ -0,0 +1,763 @@ +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +use serde::{Serialize, Deserialize}; +use std::path::Path; +use std::fs; +use std::io::Read; +use tauri::command; +use zip::ZipArchive; +use yaml_rust::{YamlLoader, Yaml}; +use std::fs::File; +use sha2::{Sha256, Digest}; +use reqwest; +use std::error::Error; + +// Add the crawlers module +mod crawlers; +use crawlers::HangarCrawler; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum ServerType { + Paper, + Spigot, + Bukkit, + Vanilla, + Forge, + Fabric, + Velocity, + BungeeCord, + Waterfall, + Unknown, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ServerInfo { + server_type: ServerType, + minecraft_version: Option, + plugins_directory: String, + plugins_count: usize, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Plugin { + name: String, + version: String, + latest_version: Option, + description: Option, + authors: Vec, + has_update: bool, + api_version: Option, + main_class: Option, + depend: Option>, + soft_depend: Option>, + load_before: Option>, + commands: Option, + permissions: Option, + file_path: String, + file_hash: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PluginMeta { + pub name: String, + pub version: String, + pub description: Option, + pub authors: Vec, + pub api_version: Option, + pub main_class: Option, + pub depend: Option>, + pub soft_depend: Option>, + pub load_before: Option>, + pub commands: Option, + pub permissions: Option, + pub file_path: String, + pub file_size: u64, + pub file_hash: String, +} + +/// Calculates SHA-256 hash for a given file path +pub fn calculate_file_hash(file_path: &str) -> Result { + let mut file = File::open(file_path).map_err(|e| format!("Failed to open file for hashing: {}", e))?; + let mut hasher = Sha256::new(); + let mut buffer = [0; 1024]; + + loop { + let bytes_read = file.read(&mut buffer).map_err(|e| format!("Failed to read file for hashing: {}", e))?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + + let hash = hasher.finalize(); + Ok(format!("{:x}", hash)) +} + +/// Extract metadata from a plugin.yml file inside a JAR +fn extract_plugin_metadata(jar_path: &Path) -> Result { + let file = fs::File::open(jar_path) + .map_err(|e| format!("Failed to open JAR file: {}", e))?; + + let file_size = file.metadata() + .map_err(|e| format!("Failed to read file metadata: {}", e))? + .len(); + + let mut archive = ZipArchive::new(file) + .map_err(|e| format!("Invalid JAR file: {}", e))?; + + // Try to find and read plugin.yml or bungee.yml + let yaml_content = match read_yaml_from_archive(&mut archive, "plugin.yml") { + Ok(content) => content, + Err(_) => match read_yaml_from_archive(&mut archive, "bungee.yml") { + Ok(content) => content, + Err(_) => { + // If no plugin metadata file is found, try to infer from filename + let filename = jar_path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.jar"); + + // Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8") + let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect(); + let version = if parts.len() > 1 { + parts.pop().unwrap_or("1.0.0").to_string() + } else { + "1.0.0".to_string() + }; + + let name = parts.join("-"); + + return Ok(PluginMeta { + name, + version, + description: None, + authors: Vec::new(), + api_version: None, + main_class: None, + depend: None, + soft_depend: None, + load_before: None, + commands: None, + permissions: None, + file_path: jar_path.to_string_lossy().to_string(), + file_size, + file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()), + }); + } + } + }; + + // Parse the YAML content + let docs = match YamlLoader::load_from_str(&yaml_content) { + Ok(docs) => docs, + Err(e) => { + println!("Failed to parse plugin YAML: {}", e); + return fallback_plugin_meta(jar_path, file_size); + } + }; + + if docs.is_empty() { + return fallback_plugin_meta(jar_path, file_size); + } + + let doc = &docs[0]; + + // Extract basic metadata with fallbacks for missing fields + let name = yaml_str_with_fallback(doc, "name", jar_path); + let version = yaml_str_with_fallback(doc, "version", jar_path); + + // Extract optional fields + let description = yaml_str_opt(doc, "description"); + + // Handle authors (can be a single string or an array) + let authors = match &doc["authors"] { + Yaml::Array(arr) => { + arr.iter() + .filter_map(|a| a.as_str().map(|s| s.to_string())) + .collect() + }, + Yaml::String(s) => vec![s.clone()], + _ => { + // Fallback to 'author' field which is sometimes used + match &doc["author"] { + Yaml::String(s) => vec![s.clone()], + _ => Vec::new(), + } + } + }; + + // Extract other optional metadata + let api_version = yaml_str_opt(doc, "api-version"); + let main_class = yaml_str_opt(doc, "main"); + + // Handle dependency lists + let depend = yaml_str_array(doc, "depend"); + let soft_depend = yaml_str_array(doc, "softdepend"); + let load_before = yaml_str_array(doc, "loadbefore"); + + // Handle complex structures as generic JSON values + let commands = match &doc["commands"] { + Yaml::Hash(_) => { + Some(serde_json::Value::String("Commands data present".to_string())) + }, + _ => None + }; + + let permissions = match &doc["permissions"] { + Yaml::Hash(_) => { + Some(serde_json::Value::String("Permissions data present".to_string())) + }, + _ => None + }; + + // Calculate the file hash + let file_hash = calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()); + + Ok(PluginMeta { + name, + version, + description, + authors, + api_version, + main_class, + depend, + soft_depend, + load_before, + commands, + permissions, + file_path: jar_path.to_string_lossy().to_string(), + file_size, + file_hash, + }) +} + +// Helper function to read a YAML file from the ZIP archive +fn read_yaml_from_archive(archive: &mut ZipArchive, file_name: &str) -> Result { + match archive.by_name(file_name) { + Ok(mut file) => { + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(|e| format!("Failed to read {}: {}", file_name, e))?; + Ok(contents) + }, + Err(e) => Err(format!("Failed to find {}: {}", file_name, e)) + } +} + +// Helper function to create plugin metadata with fallback values +fn fallback_plugin_meta(jar_path: &Path, file_size: u64) -> Result { + let filename = jar_path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.jar"); + + // Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8") + let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect(); + let version = if parts.len() > 1 { + parts.pop().unwrap_or("1.0.0").to_string() + } else { + "1.0.0".to_string() + }; + + let name = parts.join("-"); + + Ok(PluginMeta { + name, + version, + description: None, + authors: Vec::new(), + api_version: None, + main_class: None, + depend: None, + soft_depend: None, + load_before: None, + commands: None, + permissions: None, + file_path: jar_path.to_string_lossy().to_string(), + file_size, + file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()), + }) +} + +// Extract a string from YAML with fallback to filename +fn yaml_str_with_fallback(yaml: &Yaml, key: &str, jar_path: &Path) -> String { + match yaml[key].as_str() { + Some(s) => s.to_string(), + None => { + // Extract from filename as fallback + let filename = jar_path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.jar"); + + if key == "name" { + // Extract name (e.g., "WorldEdit-7.2.8.jar" → "WorldEdit") + let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect(); + parts[0].to_string() + } else if key == "version" { + // Extract version (e.g., "WorldEdit-7.2.8.jar" → "7.2.8") + let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect(); + if parts.len() > 1 { + parts[1].to_string() + } else { + "1.0.0".to_string() + } + } else { + String::new() + } + } + } +} + +fn yaml_str_opt(yaml: &Yaml, key: &str) -> Option { + yaml[key].as_str().map(|s| s.to_string()) +} + +fn yaml_str_array(yaml: &Yaml, key: &str) -> Option> { + match &yaml[key] { + Yaml::Array(arr) => { + let strings: Vec = arr.iter() + .filter_map(|a| a.as_str().map(|s| s.to_string())) + .collect(); + if strings.is_empty() { None } else { Some(strings) } + }, + _ => None + } +} + +/// Detect the server type based on files in the server directory +fn detect_server_type(server_path: &Path) -> ServerType { + // Check for Paper + if server_path.join("cache").join("patched_1.19.2.jar").exists() || + server_path.join("paper.yml").exists() { + return ServerType::Paper; + } + + // Check for Spigot + if server_path.join("spigot.yml").exists() { + return ServerType::Spigot; + } + + // Check for Bukkit + if server_path.join("bukkit.yml").exists() { + return ServerType::Bukkit; + } + + // Check for Forge + if server_path.join("forge-server.jar").exists() || + server_path.join("mods").exists() { + return ServerType::Forge; + } + + // Check for Fabric + if server_path.join("fabric-server-launch.jar").exists() || + (server_path.join("mods").exists() && server_path.join("fabric-server-launcher.properties").exists()) { + return ServerType::Fabric; + } + + // Check for Velocity + if server_path.join("velocity.toml").exists() { + return ServerType::Velocity; + } + + // Check for BungeeCord + if server_path.join("BungeeCord.jar").exists() || + server_path.join("config.yml").exists() { + return ServerType::BungeeCord; + } + + // Check for Waterfall + if server_path.join("waterfall.jar").exists() || + server_path.join("waterfall.yml").exists() { + return ServerType::Waterfall; + } + + // Check if it's at least a vanilla server + if server_path.join("server.properties").exists() || + server_path.join("vanilla_server.jar").exists() { + return ServerType::Vanilla; + } + + // If no server type detected + ServerType::Unknown +} + +/// Guess the Minecraft version from various files in the server directory +fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Option { + // Try from version.json if it exists + if let Ok(content) = fs::read_to_string(server_path.join("version.json")) { + if let Ok(json) = serde_json::from_str::(&content) { + if let Some(version) = json["name"].as_str() { + return Some(version.to_string()); + } + } + } + + // Try from the server jar name pattern + if let Ok(entries) = fs::read_dir(server_path) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "jar") { + let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + // Extract version from various common patterns in jar names + if filename.starts_with("paper-") || + filename.starts_with("spigot-") || + filename.starts_with("craftbukkit-") { + // Pattern: paper-1.19.2.jar, spigot-1.19.2.jar + let parts: Vec<&str> = filename.split('-').collect(); + if parts.len() > 1 { + let version_part = parts[1].trim_end_matches(".jar"); + if version_part.contains('.') { // Basic version format check + return Some(version_part.to_string()); + } + } + } + + // Look for version patterns like minecraft_server.1.19.2.jar + if filename.starts_with("minecraft_server.") { + let version_part = filename + .trim_start_matches("minecraft_server.") + .trim_end_matches(".jar"); + if version_part.contains('.') { + return Some(version_part.to_string()); + } + } + } + } + } + } + + // If server type is proxy, look in config files + if server_type == &ServerType::BungeeCord || + server_type == &ServerType::Waterfall || + server_type == &ServerType::Velocity { + // Velocity uses TOML, others use YAML + if server_type == &ServerType::Velocity { + if let Ok(content) = fs::read_to_string(server_path.join("velocity.toml")) { + // Very basic TOML parsing just for this field + for line in content.lines() { + if line.contains("minecraft-version") { + if let Some(version) = line.split('=').nth(1) { + return Some(version.trim().trim_matches('"').to_string()); + } + } + } + } + } else { + // Try to parse config.yml for BungeeCord/Waterfall + if let Ok(content) = fs::read_to_string(server_path.join("config.yml")) { + if let Ok(docs) = YamlLoader::load_from_str(&content) { + if !docs.is_empty() { + let doc = &docs[0]; + if let Some(version) = doc["minecraft_version"].as_str() { + return Some(version.to_string()); + } + } + } + } + } + } + + // Default fallback + None +} + +/// Get plugins directory path based on server type +fn get_plugins_directory(server_path: &Path, server_type: &ServerType) -> String { + match server_type { + ServerType::Velocity => server_path.join("plugins").to_string_lossy().to_string(), + ServerType::BungeeCord => server_path.join("plugins").to_string_lossy().to_string(), + ServerType::Waterfall => server_path.join("plugins").to_string_lossy().to_string(), + _ => server_path.join("plugins").to_string_lossy().to_string(), + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ScanResult { + server_info: ServerInfo, + plugins: Vec, +} + +#[command] +fn scan_server_directory(path: &str) -> Result { + let server_path = Path::new(path); + + if !server_path.exists() { + return Err(format!("Server path does not exist: {}", path)); + } + + // Detect server type and version + let server_type = detect_server_type(server_path); + let minecraft_version = detect_minecraft_version(server_path, &server_type); + + println!("Detected server type: {:?}", server_type); + if let Some(version) = &minecraft_version { + println!("Detected Minecraft version: {}", version); + } + + // Determine plugins directory based on server type + let plugins_dir_str = get_plugins_directory(server_path, &server_type); + let plugins_dir = Path::new(&plugins_dir_str); + + if !plugins_dir.exists() { + return Err(format!("Plugins directory not found at: {}", plugins_dir.display())); + } + + // Scan for JAR files in the plugins directory + let mut plugins = Vec::new(); + + match fs::read_dir(&plugins_dir) { + Ok(entries) => { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + + // Check if this is a JAR file + if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) { + match extract_plugin_metadata(&path) { + Ok(meta) => { + // Create a Plugin from PluginMeta + let plugin = Plugin { + name: meta.name, + version: meta.version, + latest_version: None, // Will be filled by update checker + description: meta.description, + authors: meta.authors, + has_update: false, // Will be determined by update checker + api_version: meta.api_version, + main_class: meta.main_class, + depend: meta.depend, + soft_depend: meta.soft_depend, + load_before: meta.load_before, + commands: meta.commands, + permissions: meta.permissions, + file_path: meta.file_path, + file_hash: meta.file_hash, + }; + + plugins.push(plugin); + }, + Err(e) => { + // Log error but continue with other plugins + println!("Error reading plugin from {}: {}", path.display(), e); + } + } + } + } + } + }, + Err(e) => { + return Err(format!("Failed to read plugins directory: {}", e)); + } + } + + // If no plugins were found, fall back to mock data for testing + if plugins.is_empty() && server_type == ServerType::Unknown { + // For testing only - in production, we'd just return an empty list + plugins = vec![ + Plugin { + name: "EssentialsX".to_string(), + version: "2.19.0".to_string(), + latest_version: Some("2.20.0".to_string()), + description: Some("Essential server tools for Minecraft".to_string()), + authors: vec!["md_5".to_string(), "SupaHam".to_string()], + has_update: true, + api_version: Some("1.13".to_string()), + main_class: Some("com.earth2me.essentials.Essentials".to_string()), + depend: None, + soft_depend: None, + load_before: None, + commands: None, + permissions: None, + file_path: "EssentialsX.jar".to_string(), + file_hash: calculate_file_hash("EssentialsX.jar").unwrap_or_else(|_| "unknown".to_string()), + }, + Plugin { + name: "WorldEdit".to_string(), + version: "7.2.8".to_string(), + latest_version: Some("7.2.8".to_string()), + description: Some("In-game map editor".to_string()), + authors: vec!["sk89q".to_string(), "wizjany".to_string()], + has_update: false, + api_version: Some("1.13".to_string()), + main_class: Some("com.sk89q.worldedit.bukkit.WorldEditPlugin".to_string()), + depend: None, + soft_depend: None, + load_before: None, + commands: None, + permissions: None, + file_path: "WorldEdit.jar".to_string(), + file_hash: calculate_file_hash("WorldEdit.jar").unwrap_or_else(|_| "unknown".to_string()), + }, + ]; + } + + // Create server info + let server_info = ServerInfo { + server_type, + minecraft_version, + plugins_directory: plugins_dir_str, + plugins_count: plugins.len(), + }; + + Ok(ScanResult { + server_info, + plugins, + }) +} + +#[command] +fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +// Web Crawler Module +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum RepositorySource { + HangarMC, + SpigotMC, + Modrinth, + GitHub, + BukkitDev, + Custom(String), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RepositoryPlugin { + id: String, // Unique identifier in the repository + name: String, // Plugin name + version: String, // Latest version + description: Option, + authors: Vec, + download_url: String, // URL to download the plugin + repository: RepositorySource, + page_url: String, // URL to the plugin page + download_count: Option, + last_updated: Option, + icon_url: Option, + minecraft_versions: Vec, + categories: Vec, + rating: Option, + file_size: Option, + file_hash: Option, +} + +// Trait for implementing different repository crawlers +pub trait RepositoryCrawler { + fn search(&self, query: &str) -> Result, Box>; + fn get_plugin_details(&self, plugin_id: &str) -> Result>; + fn get_plugin_versions(&self, plugin_id: &str) -> Result, Box>; + fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result>; + fn get_repository_name(&self) -> RepositorySource; +} + +// Basic HTTP client for crawler implementations +pub struct HttpClient { + client: reqwest::blocking::Client, +} + +impl HttpClient { + pub fn new() -> Self { + let client = reqwest::blocking::Client::builder() + .user_agent("PlugSnatcher/0.1.0") + .build() + .unwrap_or_else(|_| reqwest::blocking::Client::new()); + + HttpClient { client } + } + + pub fn get(&self, url: &str) -> Result> { + let response = self.client.get(url).send()?; + + if response.status().is_success() { + Ok(response.text()?) + } else { + Err(format!("HTTP error: {}", response.status()).into()) + } + } + + pub fn download(&self, url: &str, destination: &Path) -> Result<(), Box> { + let response = self.client.get(url).send()?; + + if response.status().is_success() { + let bytes = response.bytes()?; + fs::write(destination, bytes)?; + Ok(()) + } else { + Err(format!("Download failed: {}", response.status()).into()) + } + } +} + +// Helper function to get crawler for a specific repository +fn get_crawler(repository: &RepositorySource) -> Option> { + match repository { + RepositorySource::HangarMC => Some(Box::new(HangarCrawler::new())), + // Other repositories will be implemented later + _ => None, + } +} + +// Command to search for plugins in specified repositories +#[command] +pub fn search_repository_plugins(query: &str, repositories: Vec) -> Result, String> { + let mut results: Vec = Vec::new(); + + // Try each requested repository + for repo in repositories { + if let Some(crawler) = get_crawler(&repo) { + match crawler.search(query) { + Ok(repo_results) => { + results.extend(repo_results); + }, + Err(e) => { + println!("Error searching in repository {:?}: {}", repo, e); + // Continue with other repositories even if one fails + } + } + } else { + println!("Repository crawler for {:?} not implemented yet", repo); + } + } + + if results.is_empty() { + Err("No plugins found or repositories not implemented yet".to_string()) + } else { + Ok(results) + } +} + +// Command to get plugin details from a specific repository +#[command] +pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySource) -> Result { + if let Some(crawler) = get_crawler(&repository) { + crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string()) + } else { + Err(format!("Repository crawler for {:?} not implemented yet", repository)) + } +} + +// Command to download a plugin from a repository +#[command] +pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result { + if let Some(crawler) = get_crawler(&repository) { + crawler + .download_plugin(plugin_id, version, Path::new(destination)) + .map_err(|e| e.to_string()) + } else { + Err(format!("Repository crawler for {:?} not implemented yet", repository)) + } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![ + greet, + scan_server_directory, + search_repository_plugins, + get_repository_plugin_details, + download_repository_plugin + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +}