Skip to content

Instantly share code, notes, and snippets.

@qryxip
Last active March 30, 2020 08:18
Show Gist options
  • Select an option

  • Save qryxip/bd320e1e1fee1b2f74aa0cf290f70cae to your computer and use it in GitHub Desktop.

Select an option

Save qryxip/bd320e1e1fee1b2f74aa0cf290f70cae to your computer and use it in GitHub Desktop.
//! This code is licensed under [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0).
//!
//! ```cargo
//! [package]
//! name = "atcoder-cat"
//! version = "0.0.0"
//! authors = ["Ryo Yamashita <[email protected]>"]
//! edition = "2018"
//! license = "CC0-1.0"
//! publish = false
//!
//! [dependencies]
//! anyhow = "1.0.27"
//! base64 = "0.12.0"
//! env_logger = "0.7.1"
//! log = "0.4.8"
//! maplit = "1.0.2"
//! once_cell = "1.3.1"
//! reqwest = { version = "0.10.4", default-features = false, features = ["rustls-tls", "blocking", "cookies", "gzip", "json"] }
//! rpassword = "4.0.5"
//! rprompt = "1.0.5"
//! scraper = "0.11.0"
//! serde = { version = "1.0.105", features = ["derive"] }
//! serde_json = "1.0.48"
//! shell-escape = "0.1.4"
//! structopt = "0.3.12"
//! url = "2.1.1"
//! ```
use anyhow::{bail, Context as _};
use env_logger::fmt::{Color, WriteStyle};
use log::{info, Level, LevelFilter};
use maplit::hashmap;
use once_cell::sync::Lazy;
use reqwest::redirect::Policy;
use scraper::selector::Selector;
use scraper::Html;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};
use structopt::StructOpt;
use url::Url;
use std::io::{self, Write as _};
use std::num::ParseIntError;
use std::thread;
use std::time::Duration;
macro_rules! selector(($s:expr $(,)?) => ({
static SELECTOR: Lazy<Selector> = Lazy::new(|| Selector::parse($s).unwrap());
&SELECTOR
}));
fn main() -> anyhow::Result<()> {
let Opt {
timeout,
contest,
color,
path,
} = Opt::from_args();
init_logger(color);
let username = rprompt::prompt_reply_stderr("Username: ")?;
let password = rpassword::read_password_from_tty(Some("Password: "))?;
let client = setup_client(timeout)?;
let csrf_token = get(&client, "/login", &[200])?
.html()?
.extract_csrf_token()?;
let payload = hashmap!(
"csrf_token" => csrf_token,
"username" => username,
"password" => password,
);
post_form(&client, "/login", &payload, &[302])?;
if get(&client, "/settings", &[200, 302])?.status() == 302 {
bail!("Failed to login");
}
let csrf_token = get(
&client,
&format!("/contests/{}/custom_test", contest),
&[200],
)?
.html()?
.extract_csrf_token()?;
let mut acc = vec![];
while {
let code = BASH_CODE_TEMPLATE
.replace("{{file}}", &shell_escape::escape((&path).into()))
.replace("{{offset}}", &acc.len().to_string())
.replace("{{chunk-len}}", &CHUNK_LEN.to_string());
let payload = hashmap!(
"data.LanguageId" => BASH_ID,
"sourceCode" => &code,
"input" => "",
"csrf_token" => &csrf_token,
);
post_form(
&client,
&format!("/contests/{}/custom_test/submit/json", contest),
&payload,
&[200],
)?;
loop {
let ResponsePayload { result } = get(
&client,
&format!("/contests/{}/custom_test/json", contest),
&[200],
)?
.json()?;
info!("Result.Status = {}", result.status);
if result.source_code == code.as_bytes()
&& result.input == b""
&& result.status == 3
&& result.language_id.to_string() == BASH_ID
{
if result.exit_code != 0 {
bail!(
"Failed with code {}: {:?}",
result.exit_code,
String::from_utf8_lossy(&result.error),
);
}
acc.extend_from_slice(&result.output);
break result.output.len() == CHUNK_LEN;
}
info!("Waiting {:?}...", INTERVAL);
thread::sleep(INTERVAL);
}
} {}
let mut stdout = io::stdout();
stdout.write_all(&acc)?;
stdout.flush()?;
return Ok(());
const INTERVAL: Duration = Duration::from_secs(2);
const CHUNK_LEN: usize = 2048;
static BASH_ID: &str = "4007";
static BASH_CODE_TEMPLATE: &str = r#"
FILE={{file}}
OFFSET={{offset}}
content="$(cat "$FILE" && printf '#')" && content="${content%#}" && echo -n "${content:$OFFSET:{{chunk-len}}}"
"#;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ResponsePayload {
result: ResponsePayloadResult,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ResponsePayloadResult {
#[serde(deserialize_with = "deser_b64")]
source_code: Vec<u8>,
#[serde(deserialize_with = "deser_b64")]
input: Vec<u8>,
#[serde(deserialize_with = "deser_b64")]
output: Vec<u8>,
#[serde(deserialize_with = "deser_b64")]
error: Vec<u8>,
exit_code: i32,
status: u32,
language_id: u32,
}
fn deser_b64<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let b64 = String::deserialize(deserializer)?;
base64::decode(&b64).map_err(D::Error::custom)
}
}
#[derive(StructOpt, Debug)]
struct Opt {
/// Timeout
#[structopt(long, value_name("SECS"), parse(try_from_str = parse_seconds))]
timeout: Option<Duration>,
#[structopt(long, value_name("STRING"), default_value("language-test-202001"))]
contest: String,
/// Coloring
#[structopt(
long,
value_name("WHEN"),
default_value("auto"),
possible_values(&["auto", "always", "never"]),
parse(from_str = parse_write_style_unwrap)
)]
color: WriteStyle,
/// Path to the file to retrieve
path: String,
}
fn parse_seconds(s: &str) -> std::result::Result<Duration, ParseIntError> {
s.parse().map(Duration::from_millis)
}
/// Parses `s` to a `WriteStyle`.
///
/// # Panics
///
/// Panics `s` is not "auto", "always", or "never".
fn parse_write_style_unwrap(s: &str) -> WriteStyle {
match s {
"auto" => WriteStyle::Auto,
"always" => WriteStyle::Always,
"never" => WriteStyle::Never,
_ => panic!(r#"expected "auto", "always", or "never""#),
}
}
fn init_logger(color: WriteStyle) {
env_logger::Builder::new()
.format(|buf, record| {
macro_rules! style(($fg:expr, $intense:expr) => ({
let mut style = buf.style();
style.set_color($fg).set_intense($intense);
style
}));
let color = match record.level() {
Level::Error => Color::Red,
Level::Warn => Color::Yellow,
Level::Info => Color::Cyan,
Level::Debug => Color::Green,
Level::Trace => Color::White,
};
let path = record
.module_path()
.map(|p| p.split("::").next().unwrap())
.filter(|&p| p != module_path!().split("::").next().unwrap())
.map(|p| format!(" {}", p))
.unwrap_or_default();
writeln!(
buf,
"{}{}{}{} {}",
style!(Color::Black, true).value('['),
style!(color, false).value(record.level()),
path,
style!(Color::Black, true).value(']'),
record.args(),
)
})
.filter_level(LevelFilter::Info)
.write_style(color)
.init();
}
fn setup_client(timeout: Option<Duration>) -> reqwest::Result<reqwest::blocking::Client> {
return reqwest::blocking::ClientBuilder::new()
.user_agent(USER_AGENT)
.cookie_store(true)
.redirect(Policy::none())
.referer(false)
.timeout(timeout)
.build();
static USER_AGENT: &str = "atcoder-cat <[email protected]>";
}
fn get(
client: &reqwest::blocking::Client,
path: &str,
statuses: &[u16],
) -> anyhow::Result<reqwest::blocking::Response> {
let url = url(path)?;
info!("GET {}", url);
let res = client.get(url.clone()).send()?;
info!("{}", res.status());
if !statuses.contains(&res.status().as_u16()) {
bail!("{}: expected {:?}, got {}", url, statuses, res.status());
}
Ok(res)
}
fn post_form(
client: &reqwest::blocking::Client,
path: &str,
form: &impl Serialize,
statuses: &[u16],
) -> anyhow::Result<reqwest::blocking::Response> {
let url = url(path)?;
info!("POST {}", url);
let res = client.post(url.clone()).form(form).send()?;
info!("{}", res.status());
if !statuses.contains(&res.status().as_u16()) {
bail!("{}: expected {:?}, got {}", url, statuses, res.status());
}
Ok(res)
}
fn url(path: &str) -> std::result::Result<Url, url::ParseError> {
return BASE.join(path);
static BASE: Lazy<Url> = Lazy::new(|| "https://atcoder.jp".parse().unwrap());
}
trait ResponseExt {
fn html(self) -> reqwest::Result<Html>;
}
impl ResponseExt for reqwest::blocking::Response {
fn html(self) -> reqwest::Result<Html> {
let text = self.text()?;
Ok(Html::parse_document(&text))
}
}
trait HtmlExt {
fn extract_csrf_token(&self) -> anyhow::Result<String>;
}
impl HtmlExt for Html {
fn extract_csrf_token(&self) -> anyhow::Result<String> {
(|| {
let token = self
.select(selector!("[name=\"csrf_token\"]"))
.next()?
.value()
.attr("value")?
.to_owned();
Some(token).filter(|token| !token.is_empty())
})()
.with_context(|| "failed to find the CSRF token")
.with_context(|| "failed to scrape")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment