acick 0.0.2

Tools for programming contests
Documentation
use anyhow::{anyhow, Context as _};
use lazy_static::lazy_static;
use reqwest::blocking::Client;
use reqwest::{StatusCode, Url};
use scraper::{ElementRef, Html};

use crate::service::scrape::{select, ElementRefExt as _, Fetch, Scrape};
use crate::{Config, Console, Error, Result};

mod login;
mod settings;
mod tasks;
mod tasks_print;

pub use login::{LoginPage, LoginPageBuilder};
pub use settings::{SettingsPage, SettingsPageBuilder};
pub use tasks::{TasksPage, TasksPageBuilder};
pub use tasks_print::{TasksPrintPage, TasksPrintPageBuilder};

lazy_static! {
    pub static ref BASE_URL: Url = Url::parse("https://atcoder.jp").unwrap();
}

pub trait HasHeader: Scrape {
    fn select_header(&self) -> Result<ElementRef> {
        self.find_first(select!("nav"))
            .context("Could not find header")
    }

    fn is_logged_in(&self) -> Result<bool> {
        let ret = self
            .select_header()?
            .select(select!("a.dropdown-toggle .glyphicon-cog"))
            .next()
            .is_some();
        Ok(ret)
    }

    fn current_user(&self) -> Result<String> {
        if !self.is_logged_in()? {
            return Err(Error::msg("Not logged in"));
        }
        self.select_header()?
            .select(select!("a.dropdown-toggle"))
            .nth(1)
            .ok_or_else(|| Error::msg("Could not find element"))
            .map(|elem| elem.inner_text().trim().to_owned())
    }

    fn is_logged_in_as(&self, user: &str) -> Result<bool> {
        Ok(self.is_logged_in()? && self.current_user()? == user)
    }

    fn extract_contest_name(&self) -> Option<String> {
        self.find_first(select!(".contest-title"))
            .map(|elem| elem.inner_text().trim().to_owned())
    }
}

pub trait FetchMaybeNotFound: Fetch {
    fn fetch_maybe_not_found(
        &self,
        client: &Client,
        conf: &Config,
        cnsl: &mut Console,
    ) -> Result<Html> {
        let (status, html) = self.fetch(client, conf, cnsl)?;
        match status {
            StatusCode::OK => Ok(html),
            StatusCode::NOT_FOUND if NotFoundPage(&html).is_not_found() => Err(anyhow!(
                "Could not find contest : {} .
Check if the contest id is correct.",
                conf.global_opt().contest_id
            )),
            StatusCode::NOT_FOUND if NotFoundPage(&html).is_permission_denied() => Err(anyhow!(
                "Found not participated or not started contest : {} .
Participate in the contest and wait until the contest starts.",
                conf.global_opt().contest_id
            )),
            _ => Err(Error::msg("Received invalid response")),
        }
    }
}

struct NotFoundPage<'a>(&'a Html);

impl NotFoundPage<'_> {
    fn select_alert(&self) -> Option<ElementRef> {
        self.find_first(select!(".alert-danger"))
    }

    fn alert_contains(&self, pat: &str) -> bool {
        self.select_alert()
            .map(|elem| elem.inner_text().contains(pat))
            .unwrap_or(false)
    }

    fn is_permission_denied(&self) -> bool {
        self.alert_contains("Permission denied.")
    }

    fn is_not_found(&self) -> bool {
        self.alert_contains("Contest not found.")
    }
}

impl Scrape for NotFoundPage<'_> {
    fn elem(&self) -> ElementRef {
        self.0.root_element()
    }
}