hexomc_lib/mods/
curseforge.rs1use serde::{Deserialize, Serialize};
2
3use crate::error::Result;
4use crate::mods::detector::ModLoader;
5
6const CF_BASE: &str = "https://api.curseforge.com/v1";
7
8pub struct CurseForgeClient {
9 client: reqwest::Client,
10 api_key: String,
11}
12
13impl CurseForgeClient {
14 pub fn new(api_key: impl Into<String>) -> Self {
15 Self {
16 client: reqwest::Client::new(),
17 api_key: api_key.into(),
18 }
19 }
20
21 fn headers(&self) -> reqwest::header::HeaderMap {
22 let mut headers = reqwest::header::HeaderMap::new();
23 headers.insert("x-api-key", self.api_key.parse().unwrap());
24 headers.insert("Content-Type", "application/json".parse().unwrap());
25 headers
26 }
27}
28
29#[derive(Debug, Deserialize, Serialize, Clone)]
30pub struct CfMod {
31 pub id: u64,
32 pub name: String,
33 pub slug: String,
34 pub summary: String,
35 #[serde(rename = "downloadCount")]
36 pub download_count: u64,
37 #[serde(rename = "classId", default)]
39 pub class_id: Option<u32>,
40 #[serde(default)]
41 pub links: Option<CfLinks>,
42}
43
44#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct CfLinks {
46 #[serde(rename = "websiteUrl", default)]
47 pub website_url: Option<String>,
48}
49
50#[derive(Debug, Deserialize, Serialize, Clone)]
51pub struct CfModFile {
52 pub id: u64,
53 #[serde(rename = "modId", default)]
54 pub mod_id: u64,
55 #[serde(rename = "displayName")]
56 pub display_name: String,
57 #[serde(rename = "fileName")]
58 pub file_name: String,
59 #[serde(rename = "downloadUrl")]
60 pub download_url: Option<String>,
61 #[serde(rename = "gameVersions")]
62 pub game_versions: Vec<String>,
63 #[serde(rename = "fileDate")]
64 pub file_date: String,
65 pub hashes: Vec<CfHash>,
66}
67
68#[derive(Debug, Deserialize, Serialize, Clone)]
69pub struct CfHash {
70 pub value: String,
71 pub algo: u32, }
73
74impl CfModFile {
75 pub fn sha1(&self) -> Option<&str> {
76 self.hashes
77 .iter()
78 .find(|h| h.algo == 1)
79 .map(|h| h.value.as_str())
80 }
81}
82
83fn loader_type_id(loader: &ModLoader) -> u32 {
84 match loader {
85 ModLoader::Forge => 1,
86 ModLoader::Fabric => 4,
87 ModLoader::Quilt => 5,
88 ModLoader::NeoForge => 6,
89 ModLoader::Unknown => 0,
90 }
91}
92
93impl CurseForgeClient {
94 pub async fn search_mod(
96 &self,
97 name: &str,
98 loader: &ModLoader,
99 ) -> Result<Vec<CfMod>> {
100 #[derive(Deserialize)]
101 struct Resp {
102 data: Vec<CfMod>,
103 }
104
105 let resp: Resp = self
106 .client
107 .get(format!("{}/mods/search", CF_BASE))
108 .headers(self.headers())
109 .query(&[
110 ("gameId", "432"),
111 ("searchFilter", name),
112 ("modLoaderType", &loader_type_id(loader).to_string()),
113 ("sortOrder", "desc"),
114 ])
115 .send()
116 .await?
117 .json()
118 .await?;
119
120 Ok(resp.data)
121 }
122
123 pub async fn get_mod_files(&self, mod_id: u64) -> Result<Vec<CfModFile>> {
125 #[derive(Deserialize)]
126 struct Resp {
127 data: Vec<CfModFile>,
128 }
129
130 let resp: Resp = self
131 .client
132 .get(format!("{}/mods/{}/files", CF_BASE, mod_id))
133 .headers(self.headers())
134 .send()
135 .await?
136 .json()
137 .await?;
138
139 Ok(resp.data)
140 }
141
142 pub async fn get_files(&self, file_ids: &[u64]) -> Result<Vec<CfModFile>> {
144 #[derive(Serialize)]
145 struct Body<'a> {
146 #[serde(rename = "fileIds")]
147 file_ids: &'a [u64],
148 }
149 #[derive(Deserialize)]
150 struct Resp {
151 data: Vec<CfModFile>,
152 }
153
154 let resp: Resp = self
155 .client
156 .post(format!("{}/mods/files", CF_BASE))
157 .headers(self.headers())
158 .json(&Body { file_ids })
159 .send()
160 .await?
161 .error_for_status()?
162 .json()
163 .await?;
164
165 Ok(resp.data)
166 }
167
168 pub async fn get_mods(&self, mod_ids: &[u64]) -> Result<Vec<CfMod>> {
170 #[derive(Serialize)]
171 struct Body<'a> {
172 #[serde(rename = "modIds")]
173 mod_ids: &'a [u64],
174 }
175 #[derive(Deserialize)]
176 struct Resp {
177 data: Vec<CfMod>,
178 }
179
180 let resp: Resp = self
181 .client
182 .post(format!("{}/mods", CF_BASE))
183 .headers(self.headers())
184 .json(&Body { mod_ids })
185 .send()
186 .await?
187 .error_for_status()?
188 .json()
189 .await?;
190
191 Ok(resp.data)
192 }
193
194 pub async fn get_latest_file(
196 &self,
197 mod_id: u64,
198 mc_version: &str,
199 loader: &ModLoader,
200 ) -> Result<Option<CfModFile>> {
201 let files = self.get_mod_files(mod_id).await?;
202 let loader_str = format!("{:?}", loader);
203
204 let best = files
205 .into_iter()
206 .filter(|f| {
207 f.game_versions.iter().any(|v| v == mc_version)
208 && f.game_versions
209 .iter()
210 .any(|v| v.to_lowercase() == loader_str.to_lowercase())
211 })
212 .max_by_key(|f| f.file_date.clone());
213
214 Ok(best)
215 }
216}