1use std::io::Write as _;
2
3use anyhow::{anyhow, Context as _};
4use lazy_static::lazy_static;
5use maplit::hashmap;
6use reqwest::blocking::{Client, Response};
7use reqwest::redirect::Policy;
8use reqwest::{StatusCode, Url};
9
10use crate::abs_path::AbsPathBuf;
11use crate::config::SessionConfig;
12use crate::dropbox::DbxAuthorizer;
13use crate::full::{fetch_full, TestcaseIter};
14use crate::model::{Contest, ContestId, LangName, LangNameRef, Problem, ProblemId};
15use crate::page::{ExtractCsrfToken as _, ExtractLangId as _};
16use crate::page::{
17 HasHeader as _, LoginPageBuilder, SettingsPageBuilder, SubmitPageBuilder, TasksPageBuilder,
18 TasksPrintPageBuilder, BASE_URL,
19};
20use crate::service::session::WithRetry as _;
21use crate::service::{Act, ResponseExt as _};
22use crate::web::open_in_browser;
23use crate::{Config, Console, Error, Result};
24
25lazy_static! {
28 static ref DBX_APP_KEY: &'static str = {
31 #[allow(clippy::unknown_clippy_lints)]
32 #[allow(clippy::option_env_unwrap)]
33 option_env!("ACICK_DBX_APP_KEY").unwrap()
34 };
35 static ref DBX_APP_SECRET: &'static str = {
36 #[allow(clippy::unknown_clippy_lints)]
37 #[allow(clippy::option_env_unwrap)]
38 option_env!("ACICK_DBX_APP_SECRET").unwrap()
39 };
40}
41
42static USER_AGENT: &str = concat!(
43 env!("CARGO_PKG_NAME"),
44 "-",
45 env!("CARGO_PKG_VERSION"),
46 " (",
47 env!("CARGO_PKG_REPOSITORY"),
48 ")"
49);
50static DBX_REDIRECT_PORT: u16 = 4100;
51static DBX_REDIRECT_PATH: &str = "/oauth2/callback";
52
53#[derive(Debug)]
54pub struct AtcoderActor<'a> {
55 client: Client,
56 session: &'a SessionConfig,
57}
58
59impl<'a> AtcoderActor<'a> {
60 pub fn new(session: &'a SessionConfig) -> Self {
61 let client = Client::builder()
62 .referer(false)
63 .redirect(Policy::none()) .user_agent(USER_AGENT)
65 .timeout(Some(session.timeout()))
66 .build()
67 .expect("Could not setup client. \
68 TLS backend cannot be initialized, or the resolver cannot load the system configuration.");
69 AtcoderActor { client, session }
70 }
71}
72
73impl AtcoderActor<'_> {
74 fn problem_url(contest_id: &ContestId, problem: &Problem) -> Result<Url> {
75 let path = format!("/contests/{}/tasks/{}", contest_id, &problem.url_name());
76 BASE_URL
77 .join(&path)
78 .context(format!("Could not parse problem url : {}", path))
79 }
80
81 fn submissions_url(contest_id: &ContestId) -> Result<Url> {
82 let path = format!("/contests/{}/submissions/me", contest_id);
83 BASE_URL
84 .join(&path)
85 .context(format!("Could not parse submissions url : {}", path))
86 }
87
88 fn validate_login_response(res: &Response) -> Result<()> {
89 if res.status() != StatusCode::FOUND {
90 return Err(Error::msg("Received invalid response code"));
91 }
92 Ok(())
93 }
94
95 fn validate_submit_response(res: &Response, contest_id: &ContestId) -> Result<()> {
96 if res.status() != StatusCode::FOUND {
97 return Err(Error::msg("Received invalid response code"));
98 }
99 let loc_url = res
100 .location_url(&BASE_URL)
101 .context("Could not extract redirection url from response")?;
102 if loc_url != Self::submissions_url(contest_id)? {
103 return Err(Error::msg("Found invalid redirection url"));
104 }
105 Ok(())
106 }
107
108 pub fn fetch_full(
109 contest_id: &ContestId,
110 problems: &[Problem],
111 token_path: &AbsPathBuf,
112 access_token: Option<String>,
113 conf: &Config,
114 cnsl: &mut Console,
115 ) -> Result<()> {
116 let dropbox = DbxAuthorizer::new(
118 &DBX_APP_KEY,
119 &DBX_APP_SECRET,
120 DBX_REDIRECT_PORT,
121 DBX_REDIRECT_PATH,
122 token_path,
123 )
124 .load_or_request(access_token, cnsl)?;
125
126 fetch_full(&dropbox, contest_id, problems, conf, cnsl)
127 }
128
129 pub fn load_testcases(
130 testcases_dir: AbsPathBuf,
131 sample_name: &Option<String>,
132 ) -> Result<TestcaseIter> {
133 TestcaseIter::load(testcases_dir, sample_name)
134 }
135}
136
137impl Act for AtcoderActor<'_> {
138 fn current_user(&self, cnsl: &mut Console) -> Result<Option<String>> {
139 let Self { client, session } = self;
140 let login_page = LoginPageBuilder::new(session).build(client, cnsl)?;
141 login_page.current_user()
142 }
143
144 fn login(&self, user: String, pass: String, cnsl: &mut Console) -> Result<bool> {
145 let Self { client, session } = self;
146
147 let login_page = LoginPageBuilder::new(session).build(client, cnsl)?;
149 let current_user = login_page.current_user()?;
150 if let Some(current_user) = current_user {
151 if current_user != user {
153 return Err(anyhow!("Logged in as another user: {}", current_user));
154 }
155 return Ok(false);
156 }
157
158 let csrf_token = login_page.extract_csrf_token()?;
160 let payload = hashmap!(
161 "csrf_token" => csrf_token,
162 "username" => user.as_str(),
163 "password" => pass.as_str(),
164 );
165
166 let res = client
168 .post(login_page.url()?)
169 .form(&payload)
170 .with_retry(
171 client,
172 session.cookies_path(),
173 session.retry_limit(),
174 session.retry_interval(),
175 )
176 .retry_send(cnsl)?;
177
178 Self::validate_login_response(&res).context("Login rejected by service")?;
180 let settings_page = SettingsPageBuilder::new(session).build(client, cnsl)?;
181 let current_user = settings_page.current_user()?;
182 match current_user {
183 None => Err(anyhow!("Failed to log in")),
184 Some(current_user) if current_user != user => {
185 Err(anyhow!("Logged in as another user: {}", current_user))
186 }
187 _ => Ok(true),
188 }
189 }
190
191 fn fetch(
192 &self,
193 contest_id: &ContestId,
194 problem_id: &Option<ProblemId>,
195 cnsl: &mut Console,
196 ) -> Result<(Contest, Vec<Problem>)> {
197 let Self { client, session } = self;
198
199 let tasks_page = TasksPageBuilder::new(contest_id, session).build(client, cnsl)?;
200 let contest_name = tasks_page
201 .extract_contest_name()
202 .context("Could not extract contest name")?;
203 let mut problems: Vec<Problem> = tasks_page
204 .extract_problems(cnsl)?
205 .into_iter()
206 .filter(|problem| {
207 if let Some(problem_id) = problem_id {
208 problem.id() == problem_id
209 } else {
210 true
211 }
212 })
213 .collect();
214 if problems.is_empty() {
215 let err = if let Some(problem_id) = problem_id {
216 Err(anyhow!(
217 "Could not find problem \"{}\" in contest {}",
218 problem_id,
219 contest_id
220 ))
221 } else {
222 Err(anyhow!(
223 "Could not find any problems in contest {}",
224 contest_id
225 ))
226 };
227 return err;
228 }
229
230 let tasks_print_page =
231 TasksPrintPageBuilder::new(contest_id, session).build(client, cnsl)?;
232 let mut samples_map = tasks_print_page.extract_samples_map()?;
233 for problem in problems.iter_mut() {
234 if let Some(samples) = samples_map.remove(problem.id()) {
235 problem.set_samples(samples);
236 } else {
237 return Err(anyhow!(
239 "Could not extract samples for problem : {}",
240 problem.id()
241 ));
242 }
243 }
244
245 let contest = Contest::new(contest_id.to_owned(), contest_name);
246 Ok((contest, problems))
247 }
248
249 fn submit<'a>(
250 &self,
251 contest_id: &ContestId,
252 problem: &Problem,
253 lang_names: &'a [LangName],
254 source: &str,
255 cnsl: &mut Console,
256 ) -> Result<LangNameRef<'a>> {
257 let Self { client, session } = self;
258
259 let submit_page = SubmitPageBuilder::new(contest_id, session).build(client, cnsl)?;
261
262 let (lang_id, lang_name) = lang_names
264 .iter()
265 .find_map(|lang_name| {
266 submit_page
267 .extract_lang_id(lang_name)
268 .map(|lang_id| (lang_id, lang_name))
269 })
270 .with_context(|| {
271 format!(
272 "Could not find available language from the given language list: {}",
273 lang_names.join(", ")
274 )
275 })?;
276
277 let csrf_token = submit_page.extract_csrf_token()?;
279 let payload = hashmap!(
280 "csrf_token" => csrf_token,
281 "data.TaskScreenName" => problem.url_name().as_str(),
282 "data.LanguageId" => lang_id.as_str(),
283 "sourceCode" => source,
284 );
285
286 let res = client
288 .post(submit_page.url()?)
289 .form(&payload)
290 .with_retry(
291 client,
292 session.cookies_path(),
293 session.retry_limit(),
294 session.retry_interval(),
295 )
296 .retry_send(cnsl)?;
297
298 Self::validate_submit_response(&res, contest_id)
300 .context("Submission rejected by service")?;
301
302 Ok(lang_name)
303 }
304
305 fn open_problem_url(
306 &self,
307 contest_id: &ContestId,
308 problem: &Problem,
309 cnsl: &mut Console,
310 ) -> Result<()> {
311 open_in_browser(Self::problem_url(contest_id, problem)?.as_str())?;
312 writeln!(cnsl, "Opened problem page in web browser.")?;
313 Ok(())
314 }
315
316 fn open_submissions_url(&self, contest_id: &ContestId, cnsl: &mut Console) -> Result<()> {
317 open_in_browser(Self::submissions_url(contest_id)?.as_str())?;
318 writeln!(cnsl, "Opened submissions page in web browser.")?;
319 Ok(())
320 }
321}