1use encoding_rs::SHIFT_JIS;
23
24use crate::proxy::{send_follow_redirects, send_once, url_encode, BoxError, ProxyMode};
25
26#[derive(Debug, Clone, Default)]
32pub struct PlayerResult {
33 pub order: u32,
34 pub username: String,
35 pub get_turn: i32,
36 pub rem_turn: i32,
37 pub total_point: i32,
38 pub action_point: Option<i32>,
39 pub item_point: Option<i32>,
40 pub put_point: Option<i32>,
41 pub put_damage: Option<i32>,
42}
43
44#[derive(Debug, Clone)]
46pub struct BattleResult {
47 pub room: u32,
48 pub start_time: String,
49 pub end_time: String,
50 pub players: Vec<PlayerResult>,
51}
52
53#[derive(Debug, Clone)]
55pub struct VsResultQuery {
56 pub min_start_date: String,
57 pub min_end_date: String,
58 pub min_start_time: String,
59 pub min_end_time: String,
60 pub min_room: u32,
61 pub min_turn: u32,
62 pub max_start_date: String,
63 pub max_end_date: String,
64 pub max_start_time: String,
65 pub max_end_time: String,
66 pub max_room: u32,
67 pub max_turn: u32,
68 pub min_action_point: i32,
69 pub min_item_point: i32,
70 pub min_put_point: i32,
71 pub min_put_damage: i32,
72 pub min_total_point: i32,
73 pub min_rem_turn: i32,
74 pub min_get_turn: i32,
75 pub max_action_point: i32,
76 pub max_item_point: i32,
77 pub max_put_point: i32,
78 pub max_put_damage: i32,
79 pub max_total_point: i32,
80 pub max_rem_turn: i32,
81 pub max_get_turn: i32,
82}
83
84impl VsResultQuery {
85 pub fn today() -> Self {
91 let today = chrono::Local::now().format("%Y-%m-%d").to_string();
92 Self::for_date(&today)
93 }
94
95 pub fn for_date(date: &str) -> Self {
97 Self {
98 min_start_date: date.to_string(),
99 min_end_date: date.to_string(),
100 min_start_time: "00:00:00".to_string(),
101 min_end_time: "00:00:00".to_string(),
102 min_room: 1,
103 min_turn: 1,
104 max_start_date: date.to_string(),
105 max_end_date: date.to_string(),
106 max_start_time: "23:59:59".to_string(),
107 max_end_time: "23:59:59".to_string(),
108 max_room: 10000,
109 max_turn: 8,
110 min_action_point: -1_000_000,
111 min_item_point: -1_000_000,
112 min_put_point: 0,
113 min_put_damage: -20_000_000,
114 min_total_point: -30_000_000,
115 min_rem_turn: -10_000,
116 min_get_turn: 0,
117 max_action_point: 1_000_000,
118 max_item_point: 1_000_000,
119 max_put_point: 20_000_000,
120 max_put_damage: 0,
121 max_total_point: 20_000_000,
122 max_rem_turn: 10_000,
123 max_get_turn: 10_000,
124 }
125 }
126
127 fn to_query_string(&self) -> String {
131 format!(
132 "minStartDate={}&minEndDate={}&minStartTime={}&minEndTime={}\
133 &minRoomNumber={}&minTurnNumber={}\
134 &maxStartDate={}&maxEndDate={}&maxStartTime={}&maxEndTime={}\
135 &maxRoomNumber={}&maxTurnNumber={}\
136 &minActionPoint={}&minItemPoint={}&minPutPoint={}&minPutDamage={}\
137 &minTotalPoint={}&minRemTurn={}&minGetTurn={}\
138 &maxActionPoint={}&maxItemPoint={}&maxPutPoint={}&maxPutDamage={}\
139 &maxTotalPoint={}&maxRemTurn={}&maxGetTurn={}",
140 url_encode(&self.min_start_date), url_encode(&self.min_end_date),
141 url_encode(&self.min_start_time), url_encode(&self.min_end_time),
142 self.min_room, self.min_turn,
143 url_encode(&self.max_start_date), url_encode(&self.max_end_date),
144 url_encode(&self.max_start_time), url_encode(&self.max_end_time),
145 self.max_room, self.max_turn,
146 self.min_action_point, self.min_item_point,
147 self.min_put_point, self.min_put_damage,
148 self.min_total_point, self.min_rem_turn, self.min_get_turn,
149 self.max_action_point, self.max_item_point,
150 self.max_put_point, self.max_put_damage,
151 self.max_total_point, self.max_rem_turn, self.max_get_turn,
152 )
153 }
154}
155
156impl Default for VsResultQuery {
160 fn default() -> Self {
161 Self::today()
162 }
163}
164
165fn cell_text(s: &str) -> String {
170 s.trim()
171 .replace('\u{3000}', "")
172 .replace('\u{00a0}', "")
173 .trim()
174 .to_string()
175}
176
177fn parse_opt_i32(s: &str) -> Option<i32> {
178 let t = cell_text(s);
179 if t.is_empty() { None } else { t.parse().ok() }
180}
181
182fn parse_i32(s: &str) -> i32 {
183 cell_text(s).parse().unwrap_or(0)
184}
185
186fn parse_u32(s: &str) -> u32 {
187 cell_text(s).parse().unwrap_or(0)
188}
189
190fn parse_results(html: &str) -> Vec<BattleResult> {
191 let dom = match tl::parse(html, tl::ParserOptions::default()) {
192 Ok(d) => d,
193 Err(_) => return vec![],
194 };
195 let parser = dom.parser();
196
197 let table = match dom
198 .query_selector(r#"table[border="1"]"#)
199 .and_then(|mut q| q.next())
200 .and_then(|h| h.get(parser))
201 {
202 Some(tl::Node::Tag(t)) => t,
203 _ => return vec![],
204 };
205
206 let tr_htmls: Vec<String> = table
207 .children().top().iter()
208 .filter_map(|h| h.get(parser))
209 .filter_map(|n| match n {
210 tl::Node::Tag(t) if t.name().as_bytes().eq_ignore_ascii_case(b"tbody") => {
211 Some(
212 t.children().top().iter()
213 .filter_map(|h| h.get(parser))
214 .filter_map(|n| match n {
215 tl::Node::Tag(tr)
216 if tr.name().as_bytes().eq_ignore_ascii_case(b"tr") =>
217 Some(tr.outer_html(parser).to_string()),
218 _ => None,
219 })
220 .collect::<Vec<_>>(),
221 )
222 }
223 tl::Node::Tag(t) if t.name().as_bytes().eq_ignore_ascii_case(b"tr") => {
224 Some(vec![t.outer_html(parser).to_string()])
225 }
226 _ => None,
227 })
228 .flatten()
229 .collect();
230
231 let mut results: Vec<BattleResult> = Vec::new();
232 let mut skip_header = true;
233
234 for tr_html in &tr_htmls {
235 let tr_dom = match tl::parse(tr_html, tl::ParserOptions::default()) {
236 Ok(d) => d,
237 Err(_) => continue,
238 };
239 let tp = tr_dom.parser();
240
241 let cells: Vec<String> = tr_dom
242 .query_selector("td").into_iter().flatten()
243 .filter_map(|h| h.get(tp))
244 .map(|n| {
245 fn text<'a>(node: &tl::Node<'a>, p: &'a tl::Parser<'a>) -> String {
246 match node {
247 tl::Node::Raw(b) => b.as_utf8_str().into_owned(),
248 tl::Node::Tag(t) => t.children().top().iter()
249 .filter_map(|h| h.get(p))
250 .map(|n| text(n, p))
251 .collect(),
252 _ => String::new(),
253 }
254 }
255 text(n, tp)
256 })
257 .collect();
258
259 if cells.len() < 12 {
260 continue;
261 }
262
263 if skip_header {
264 skip_header = false;
265 continue;
266 }
267
268 let start_time_text = cell_text(&cells[1]);
269 let is_new_battle = !start_time_text.is_empty();
270
271 if is_new_battle {
272 let player = PlayerResult {
273 order: parse_u32(&cells[3]),
274 username: cell_text(&cells[4]),
275 get_turn: parse_i32(&cells[5]),
276 rem_turn: parse_i32(&cells[6]),
277 total_point: parse_i32(&cells[7]),
278 action_point: parse_opt_i32(&cells[8]),
279 item_point: parse_opt_i32(&cells[9]),
280 put_point: parse_opt_i32(&cells[10]),
281 put_damage: parse_opt_i32(&cells[11]),
282 };
283 let room = {
287 let r = parse_u32(&cells[0]);
288 if r > 0 { r } else { results.last().map(|b| b.room).unwrap_or(0) }
289 };
290 results.push(BattleResult {
291 room,
292 start_time: start_time_text,
293 end_time: cell_text(&cells[2]),
294 players: vec![player],
295 });
296 } else if let Some(last) = results.last_mut() {
297 last.players.push(PlayerResult {
298 order: parse_u32(&cells[3]),
299 username: cell_text(&cells[4]),
300 get_turn: parse_i32(&cells[5]),
301 rem_turn: parse_i32(&cells[6]),
302 total_point: parse_i32(&cells[7]),
303 action_point: parse_opt_i32(&cells[8]),
304 item_point: parse_opt_i32(&cells[9]),
305 put_point: parse_opt_i32(&cells[10]),
306 put_damage: parse_opt_i32(&cells[11]),
307 });
308 }
309 }
310
311 results
312}
313
314const SERVER_BASE_URL: &str = "http://www7019ug.sakura.ne.jp/CHaserOnline003/Server/";
319const SERVER_CHECK_URL: &str = "http://www7019ug.sakura.ne.jp/CHaserOnline003/Server/UserCheck";
320const LIST_RESULT_URL: &str = "http://www7019ug.sakura.ne.jp/CHaserOnline003/Server/listvsresult";
321
322pub async fn fetch_vs_result(
334 user: &str,
335 pass: &str,
336 query: VsResultQuery,
337 proxy_uri: Option<&str>,
338) -> Result<Vec<BattleResult>, BoxError> {
339 let proxy_mode = ProxyMode::from_option(proxy_uri);
340
341 let (_, jsession) =
343 send_follow_redirects(SERVER_BASE_URL, &[], &proxy_mode).await?;
344 let jsessionid = jsession.ok_or("JSESSIONID not found")?;
345 let cookie = format!("JSESSIONID={}", jsessionid);
346
347 let check_url = format!(
350 "{}?user={}&pass={}&select=vsresultview",
351 SERVER_CHECK_URL,
352 url_encode(user),
353 url_encode(pass),
354 );
355 let _ = send_once(&check_url, &[("Cookie", cookie.clone())], &proxy_mode).await?;
356
357 let list_url = format!("{}?{}", LIST_RESULT_URL, query.to_query_string());
359 let (_, _, body) = send_once(&list_url, &[("Cookie", cookie)], &proxy_mode).await?;
360
361 let (html, _, _) = SHIFT_JIS.decode(&body);
363 Ok(parse_results(&html))
364}