caracal 0.3.3

Nostr client for Gemini
#![allow(dead_code)]

mod aska;
mod certgen;
mod config;
mod context;
mod emoji;
mod events;
mod hookstr;
mod keyutil;
mod nips;
mod nostru;
mod relays;
mod rendering;
mod routes;
pub mod storage;
mod ts;
mod urls;
mod user;
mod util;

use clap::Arg;
use qpidfile::Pidfile;
use standard_paths::{LocationType, StandardPaths};
use std::path::PathBuf;

use routes::guardian;

use hookstr::Hookstr;

use windmark_titanesque::response::Response;

use nostr_sdk::prelude::*;

rust_i18n::i18n!("locales");

pub fn parse_args() -> clap::ArgMatches {
    clap::Command::new(env!("CARGO_PKG_NAME"))
        .version(env!("CARGO_PKG_VERSION"))
        .arg(
            Arg::new("port")
                .default_value("1965")
                .value_parser(clap::value_parser!(i32))
                .short('p')
                .help("Gemini service port number"),
        )
        .arg(
            Arg::new("pid-file")
                .default_value("caracal.pid")
                .long("pid-file")
                .help("Process id (pid) file path"),
        )
        .get_matches()
}

macro_rules! mkroute {
    ($handler:expr) => {
        move |ctx| guardian(ctx, $handler)
    };
}

#[windmark_titanesque::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let sp = StandardPaths::without_org(env!("CARGO_PKG_NAME"));

    let matches = parse_args();
    let port = matches.get_one::<i32>("port").copied().unwrap();
    let pid_file_path =
        PathBuf::from(matches.get_one::<String>("pid-file").unwrap());

    let _pidfile = Pidfile::new(pid_file_path);

    let app_data_path = sp
        .writable_location(LocationType::AppLocalDataLocation)
        .expect("Cannot find a writable location for data files");

    let gemini_data_path = app_data_path.join("gemini");

    std::fs::create_dir_all(gemini_data_path.clone())?;

    let cert_path = gemini_data_path.join("cert.pem");
    let key_path = gemini_data_path.join("key.pem");

    if !cert_path.exists() || !key_path.exists() {
        certgen::gen_cert(
            vec!["localhost".to_string()],
            &cert_path,
            &key_path,
        )?;
    }

    windmark_titanesque::router::Router::new()
        .set_port(port)
        .set_private_key_file(key_path.display().to_string())
        .set_certificate_file(cert_path.display().to_string())
        .attach_async(Hookstr::default())
        .mount("/", mkroute!(routes::index))
        // Public bookmarks list
        .mount("/bookmarks/public", mkroute!(routes::public_bookmarks))
        // Delete event from public bookmarks
        .mount("/bookmarks/public/event/:event_id/delete", mkroute!(routes::public_bookmarks_delete_event))
        .mount("/manual", routes::manual_root)
        .mount("/contacts", mkroute!(routes::contact_list_index))
        .mount("/connect", routes::nostr_connect_index)
        .mount("/connect/bunker", routes::nostr_connect_change)
        // Search
        .mount("/search/notes", routes::search_content)
        .mount("/search/notes/:searchq", routes::search_content)
        .mount("/search/notes/:searchq/:page", routes::search_content)
        // Feed
        .mount("/feed", mkroute!(routes::feed))
        .mount("/feed/:limit", mkroute!(routes::feed))
        // Follow
        .mount("/follow", mkroute!(routes::follow))
        .mount("/follow/:npub", mkroute!(routes::follow))
        // Hashtags
        .mount("/hashtags", mkroute!(routes::hashtags_index))
        .mount(
            "/hashtags/:hashtag/list",
            mkroute!(routes::notes_by_hashtag),
        )
        .mount(
            "/hashtags/:hashtag/notes",
            mkroute!(routes::notes_by_hashtag),
        )
        .mount("/hashtags/:hashtag/post", mkroute!(routes::post_note))
        .mount("/hashtags/:hashtag/bookmark", mkroute!(routes::bookmark_public_add))
        // Post text note
        .mount("/post", mkroute!(routes::post_note))
        .mount("/post/pow/:pow", mkroute!(routes::post_note))
        // Profile
        .mount("/profile", mkroute!(routes::profile_index))
        .mount("/profile/upload/picture", mkroute!(routes::profile_upload))
        .mount("/profile/upload/banner", mkroute!(routes::profile_upload))
        .mount("/profile/edit/:attr", mkroute!(routes::profile_edit))
        .mount("/profile/reset", mkroute!(routes::profile_init_reset))
        .mount("/profile/init", mkroute!(routes::profile_init_reset))
        .mount("/p/:npub", mkroute!(routes::person_profile))
        .mount("/p/:npub/follow", mkroute!(routes::person_follow))
        .mount("/p/:npub/mute", mkroute!(routes::person_mute))
        .mount("/p/:npub/unmute", mkroute!(routes::person_unmute))
        .mount("/p/:npub/unfollow", mkroute!(routes::person_unfollow))
        .mount("/p/:npub/send_dm", routes::person_send_dm)
        .mount("/p/:npub/conversation", mkroute!(routes::person_dm_conversation))
        .mount("/dm/all", mkroute!(routes::dm_all))
        .mount("/people", mkroute!(routes::nostriches_list_all))
        .mount("/people/recent", mkroute!(routes::nostriches_list_recent))
        .mount("/people/search", mkroute!(routes::nostriches_search))
        // Note
        .mount("/note/:event_id/bookmark", mkroute!(routes::bookmark_public_add))
        .mount("/note/:event_id/thread", mkroute!(routes::note_thread))
        .mount("/note/:event_id/reply", mkroute!(routes::note_reply))
        .mount("/note/:event_id/repost", mkroute!(routes::note_repost))
        .mount("/note/:event_id/react", mkroute!(routes::note_react_custom))
        .mount("/note/:event_id/react_select", mkroute!(routes::note_react_select))
        // Draft
        .mount("/draft/new", mkroute!(routes::draft_new))
        .mount("/draft/:draft_id/edit", mkroute!(routes::draft_edit))
        .mount("/draft/:draft_id/post", mkroute!(routes::draft_post))
        .mount(
            "/draft/:draft_id/append",
            mkroute!(routes::draft_body_append),
        )
        .mount(
            "/draft/:draft_id/append/:text",
            mkroute!(routes::draft_body_append),
        )
        .mount(
            "/draft/:draft_id/upload_file",
            mkroute!(routes::draft_upload_file),
        )
        // Inbox/outbox
        .mount("/inbox", mkroute!(routes::inbox))
        .mount("/outbox", mkroute!(routes::outbox))
        // Nsite
        .mount("/nsite/ls", mkroute!(routes::nsites_index))
        // Polls
        .mount("/polls", mkroute!(routes::polls_list))
        .mount(
            "/polls/:event_id/respond/:option_id",
            mkroute!(routes::poll_respond),
        )
        .mount("/polls/:event_id", mkroute!(routes::poll_details))
        // Settings
        .mount("/settings", mkroute!(routes::settings_index))
        .mount("/settings/relays", mkroute!(routes::relays_settings))
        .mount("/settings/relays/:mode/:op", mkroute!(routes::relay_modify))
        .mount("/settings/relays/:mode/add_from_list", mkroute!(routes::relays_add_from_popular))
        .mount("/settings/ui", mkroute!(routes::ui_settings))
        .mount("/settings/ui/change_locale/:locale", mkroute!(routes::ui_settings_chlocale))
        // Routes to serve web content
        .mount("/www/get/:filename", routes::route_www_get)
        // Use an image extension in the route path so that lagrange knows it's an image
        .mount("/www/get_image.png", routes::route_www_get)
        .mount("/db/purge", mkroute!(routes::db_purge))
        // Images
        .mount("/images/caracal.png", |_| {
            Response::binary_success(include_bytes!("../media/img/caracal.png"), "image/png")
        })
        .mount("/about", routes::about)
        // Footer
        .add_footer(|_context: &windmark_titanesque::context::RouteContext| {
            format!("\n\ncaracal version {}", env!("CARGO_PKG_VERSION"))
        })
        .set_error_handler(|_| Response::permanent_failure("Error!"))
        .run()
        .await
}