/* * asklyphe-frontend main.rs * - entrypoint for the asklyphe frontend * * Copyright (C) 2025 Real Microsoft, LLC * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ pub mod searchbot; pub mod wikipedia; pub mod unit_converter; pub mod bangs; pub mod routes; use std::{env, process}; use std::collections::HashMap; use std::net::SocketAddr; use std::ops::Deref; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use askama::Template; use asklyphe_common::nats; use asklyphe_common::nats::comms; use asklyphe_common::nats::searchservice::{SearchSrvcQuery, SearchSrvcRequest, SearchSrvcResponse}; use async_nats::jetstream; use axum::{Extension, Router}; use axum::extract::Query; use axum::http::{HeaderValue, Method}; use axum::response::IntoResponse; use axum::routing::{get, post}; use serde::Serialize; use tokio::sync::Mutex; use tower_http::cors::{AllowOrigin, CorsLayer}; use tracing::error; use tracing_loki::url::Url; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use crate::routes::admin::{admin_announcement, admin_announcement_post, admin_home, admin_invitecode, admin_invitecode_post, admin_user_list}; use crate::routes::announcement::announcement_full; use crate::routes::index::{frontpage, index, logout}; use crate::routes::search::{image_proxy, search, search_json}; use crate::routes::semaphore::semaphore; use crate::routes::user_settings::{theme_change_post, user_settings}; use crate::unit_converter::UnitConversion; use crate::wikipedia::WikipediaSummary; const ALPHA: bool = { let alpha = option_env!("ALPHA"); alpha.is_some() }; const VERSION: &str = env!("CARGO_PKG_VERSION"); const GIT_COMMIT: &str = env!("GIT_COMMIT"); const BUILT_ON: &str = env!("BUILT_ON"); const YEAR: &str = env!("YEAR"); #[derive(Clone)] pub struct Opts { pub bind_addr: SocketAddr, pub nats_addr: SocketAddr, nats_cert: String, nats_key: String, auth_url: String, pub loki_addr: Option, pub datacenter_id: i64, pub emergency: bool, } pub static WEBSITE_COUNT: AtomicU64 = AtomicU64::new(0); async fn website_count_update_thread(nats: Arc) { loop { searchbot::update_website_counter(nats.clone()).await; tokio::time::sleep(Duration::from_secs(30)).await; } } #[tokio::main] async fn main() { env_logger::init(); let opts = Opts { bind_addr: env::var("BIND_ADDR").unwrap_or("0.0.0.0:5842".to_string()).parse().expect("Badly formed BIND_ADDR (Needs to be SocketAddr)"), nats_addr: env::var("NATS_ADDR").unwrap_or("127.0.0.1:4222".to_string()).parse().expect("Badly formed NATS_ADDR (Needs to be SocketAddr)"), nats_cert: env::var("NATS_CERT").expect("NATS_CERT needs to be set"), nats_key: env::var("NATS_KEY").expect("NATS_KEY needs to be set"), auth_url: env::var("AUTH_URL").unwrap_or("https://auth.asklyphe.com".to_string()), loki_addr: match env::var("LOKI_ADDR") { Ok(url) => { Some(Url::parse(&url).expect("Badly formed LOKI_ADDR (Needs to be Url)")) } Err(_) => { None } }, datacenter_id: env::var("DATACENTER_ID").unwrap_or("1".to_string()).parse().expect("Badly formed DATACENTER_ID (Need to be i64)"), emergency: env::var("EMERGENCY").unwrap_or("0".to_string()).eq("1"), }; if opts.loki_addr.is_some() { //let (layer, task) = tracing_loki::builder() // .label("environment", "dev").unwrap() // .extra_field("pid", process::id().to_string()).unwrap() // .extra_field("run_id", snowflake_factory.generate().to_string()).unwrap() // .build_url(opts.loki_addr.clone().unwrap()).unwrap(); //// Register Loki layer //tracing_subscriber::registry() // .with(layer) // .init(); //// Spawn Loki background task //tokio::spawn(task); } else { //tracing_subscriber::fmt::init() } let nats = async_nats::ConnectOptions::new() .add_client_certificate(opts.nats_cert.as_str().into(), opts.nats_key.as_str().into()) .connect(opts.nats_addr.to_string()) .await; if let Err(e) = nats { eprintln!("FATAL ERROR, COULDN'T CONNECT TO NATS: {}", e); return; } let nats = nats.unwrap(); let nats = jetstream::new(nats); let nats = Arc::new(nats); tokio::spawn(website_count_update_thread(nats.clone())); let app = Router::new() .route("/", get(index)) .route("/semaphore", get(semaphore)) .route("/frontpage", get(frontpage)) .route("/ask", get(search)) .route("/ask_json", get(search_json)) .route("/imgproxy", get(image_proxy)) .route("/logout", post(logout)) .route("/user_settings", get(user_settings)) .route("/user_settings/set_theme", post(theme_change_post)) .route("/admin", get(admin_home)) .route("/admin/invitecode", get(admin_invitecode).post(admin_invitecode_post)) .route("/admin/announcement", get(admin_announcement).post(admin_announcement_post)) .route("/admin/allusers", get(admin_user_list)) .route("/announcements/:slug", get(announcement_full)) .layer(CorsLayer::new().allow_methods([Method::GET, Method::POST]).allow_origin(AllowOrigin::exact(HeaderValue::from_str("localhost").unwrap()))) .layer(Extension(nats.clone())) .layer(Extension(opts.clone())) .fallback(routes::not_found); let listener = tokio::net::TcpListener::bind(opts.bind_addr).await.expect("Failed to get listener"); axum::serve(listener, app).await.expect("Failed to serve"); }