use axum::Form; use axum::{ body::Bytes, extract::State, http::{header, HeaderMap, StatusCode}, response::{Html, IntoResponse}, routing::{get, post}, Json, Router, }; use chrono::{DateTime, NaiveDateTime, Utc}; use chrono_humanize::HumanTime; use ramhorns::{Content, Template}; use serde::Deserialize; use std::collections::BTreeMap; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; use std::net::SocketAddr; use url::Url; #[derive(Content)] struct HomePage { title: String, description: String, source: String, version: String, } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let app = Router::new() .route("/", get(root)) .route("/search", post(search)) .route("/style.css", get(stylesheet)); let addr = SocketAddr::from(([127, 0, 0, 1], 3001)); tracing::debug!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } async fn stylesheet() -> impl IntoResponse { let mut headers = HeaderMap::new(); headers.insert(header::CONTENT_TYPE, "text/css".parse().unwrap()); let mut file = File::open("static/style.css").unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); (headers, contents) } async fn root() -> Html { let mut source = File::open("static/home.html").unwrap(); let mut contents = String::new(); source.read_to_string(&mut contents).unwrap(); let tpl = Template::new(contents).unwrap(); let rendered = tpl.render(&HomePage { title: "Ferret".to_string(), description: "A small independent search engine".to_string(), source: "https://git.lavender.software/erin/ferret".to_string(), version: "v0.2.0".to_string(), }); Html(rendered) } #[derive(Deserialize)] struct SearchForm { query: String, } #[derive(Content)] struct RenderSearchResult { url: String, size: i64, title: String, summary: String, last_updated: String, } #[derive(Debug, Deserialize)] struct SearchResult { url: Url, size: i64, title: String, summary: String, last_updated: DateTime, } #[derive(Content)] struct SearchResults { title: String, description: String, query: String, results: Vec, } async fn search(Form(search): Form) -> Html { let mut source = File::open("static/results.html").unwrap(); let mut contents = String::new(); source.read_to_string(&mut contents).unwrap(); let mut map = HashMap::new(); map.insert("language", "eng"); map.insert("include", &search.query); map.insert("option", "Fuzzy"); let client = reqwest::Client::new(); let res = client .get("http://127.0.0.1:3000/api/search") .json(&map) .send() .await .unwrap() .json::>() .await .unwrap(); tracing::info!("{:#?}", res); let mut results = Vec::new(); for (i, r) in res.iter().rev() { results.push(RenderSearchResult { url: r.url.as_str().to_string(), size: r.size, title: r.title.clone(), summary: r.summary.clone(), last_updated: HumanTime::from(r.last_updated).to_string(), }); } let tpl = Template::new(contents).unwrap(); let rendered = tpl.render(&SearchResults { title: "Ferret".to_string(), description: "A small independent search engine".to_string(), query: search.query, results, }); Html(rendered) }