novel-api 0.19.1

Novel APIs from various sources
Documentation
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use askama::Template;
use axum::extract::{self, State};
use axum::http::{StatusCode, header};
use axum::response::{Html, IntoResponse, Response};
use axum::{Router, routing};
use rust_embed::RustEmbed;
use tokio::net::TcpListener;
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::oneshot;
use tokio::task;

use super::GeetestInfoResponse;
use crate::Error;

#[derive(RustEmbed)]
#[folder = "templates"]
struct Asset;

struct StaticFile<T>(pub T);

impl<T> IntoResponse for StaticFile<T>
where
    T: Into<String>,
{
    fn into_response(self) -> Response {
        let path = self.0.into();

        match Asset::get(path.as_str()) {
            Some(content) => {
                let mime = mime_guess::from_path(path).first_or_octet_stream();
                ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
            }
            None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
        }
    }
}

impl IntoResponse for Error {
    fn into_response(self) -> Response {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Something went wrong: {self}"),
        )
            .into_response()
    }
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    gt: String,
    challenge: String,
    new_captcha: bool,
}

async fn captcha(
    State(state): State<(GeetestInfoResponse, Sender<String>)>,
) -> Result<Html<String>, Error> {
    let (info, _) = state;

    Ok(IndexTemplate {
        gt: info.gt,
        challenge: info.challenge,
        new_captcha: info.new_captcha,
    }
    .render()?
    .into())
}

async fn geetest_js() -> StaticFile<&'static str> {
    StaticFile("geetest.js")
}

async fn validate(
    extract::Path(validate): extract::Path<String>,
    State(state): State<(GeetestInfoResponse, Sender<String>)>,
) -> Html<&'static str> {
    let (_, tx) = state;
    tx.send(validate).await.unwrap();

    Html("Verification is successful, you can close the browser now")
}

pub(crate) async fn run_geetest(info: GeetestInfoResponse) -> Result<String, Error> {
    let (tx, mut rx) = mpsc::channel(1);

    let app = Router::new()
        .route("/captcha", routing::get(captcha))
        .route("/geetest.js", routing::get(geetest_js))
        .route("/validate/:validate", routing::get(validate))
        .with_state((info, tx));

    let addr = SocketAddr::new(
        IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
        portpicker::pick_unused_port().ok_or(Error::Port(String::from("No ports free")))?,
    );
    let listener = TcpListener::bind(addr).await?;

    let (stop_tx, stop_rx) = oneshot::channel();

    task::spawn(async move {
        axum::serve(listener, app)
            .with_graceful_shutdown(async {
                stop_rx.await.ok();
            })
            .await?;

        Ok::<_, Error>(())
    });

    open::that(format!("http://{}:{}/captcha", addr.ip(), addr.port()))?;

    let validate = rx.recv().await.unwrap();
    stop_tx.send(()).unwrap();

    Ok(validate)
}