caracal 0.4.1

Nostr client for Gemini
use super::route_prelude::*;
use crate::hookstr::DEFAULT_RELAYS;
use std::borrow::Cow;

mod filters {
    use regex::{Captures, Regex};

    #[askama::filter_fn]
    pub fn strip_hashtags(
        s: &String,
        _env: &dyn askama::Values,
    ) -> askama::Result<String> {
        let re = Regex::new(r"#(\w+)").unwrap();

        Ok(re
            .replace_all(s, |caps: &Captures| caps[1].to_string())
            .into())
    }
}

#[derive(Template, Clone)]
#[template(path = "polls_list.gmi", escape = "txt")]
struct PollsTemplate {
    polls: Events,
}

#[derive(Template, Clone)]
#[template(path = "poll.gmi", escape = "txt")]
struct PollTemplate {
    poll: Event,
    responses: Events,
}

/// Route to list nostr polls
pub async fn polls_list(
    _ctx: RouteContext,
    user: &'static mut CaracalUser,
) -> Response {
    let polls_filter = Filter::new()
        .kind(Kind::Poll)
        .since(Timestamp::now() - dur_parse("60d").unwrap());

    let Ok(polls) = user
        .client
        .fetch_combined_events(polls_filter, dur_parse("5s").unwrap())
        .await
    else {
        return Response::temporary_failure(t!("fetch_polls_failed"));
    };

    Response::success(WindTemplate::render(PollsTemplate { polls }))
}

/// Route to list all the information available about a poll
pub async fn poll_details(
    ctx: RouteContext,
    user: &'static mut CaracalUser,
) -> Response {
    let Some(event_id_s) = ctx.parameters.get("event_id") else {
        return Response::temporary_failure(t!("invalid_params"));
    };

    let Ok(event_id) = EventId::parse(event_id_s) else {
        return Response::temporary_failure(t!("invalid_params"));
    };

    let poll_filter = Filter::new().kind(Kind::Poll).id(event_id);

    let Ok(events) = user
        .client
        .fetch_combined_events(poll_filter, dur_parse("5s").unwrap())
        .await
    else {
        return Response::temporary_failure(t!("fetch_polls_failed"));
    };

    let Some(poll) = events.first_owned() else {
        return Response::temporary_failure(t!("fetch_polls_failed"));
    };

    let poll_relays: Vec<RelayUrl> = poll
        .tags
        .filter_standardized(TagKind::Custom(Cow::Borrowed("relay")))
        .filter_map(|tag| {
            if let TagStandard::Relay(url) = tag {
                Some(url.clone())
            } else {
                None
            }
        })
        .collect();

    for relay in &poll_relays {
        let _ = user.client.add_read_relay(relay).await;
    }

    let relays = if poll_relays.is_empty() {
        // No relays specified in the poll event, use default relays
        DEFAULT_RELAYS
            .iter()
            .map(|u| RelayUrl::parse(u).unwrap())
            .collect()
    } else {
        poll_relays
    };

    let resp_filter = Filter::new().kind(Kind::PollResponse).event(event_id);

    let Ok(responses) = user
        .client
        .fetch_events_from(relays, resp_filter, dur_parse("5s").unwrap())
        .await
    else {
        return Response::temporary_failure(t!("fetch_polls_failed"));
    };

    Response::success(WindTemplate::render(PollTemplate { poll, responses }))
}

/// Route to respond to a poll
pub async fn poll_respond(
    ctx: RouteContext,
    user: &'static mut CaracalUser,
) -> Response {
    let Some(event_id_s) = ctx.parameters.get("event_id") else {
        return Response::temporary_failure(t!("invalid_params"));
    };

    let Ok(event_id) = EventId::parse(event_id_s) else {
        return Response::temporary_failure(t!("invalid_params"));
    };

    let Some(option_id) = ctx.parameters.get("option_id") else {
        return Response::temporary_failure(t!("invalid_params"));
    };

    let poll_filter = Filter::new().kind(Kind::Poll).id(event_id);

    let Ok(events) = user
        .client
        .fetch_events(poll_filter, dur_parse("5s").unwrap())
        .await
    else {
        return Response::temporary_failure(t!("fetch_polls_failed"));
    };

    let Some(poll) = events.first() else {
        return Response::temporary_failure(t!("fetch_polls_failed"));
    };

    let relays: Vec<RelayUrl> = poll
        .tags
        .filter_standardized(TagKind::Custom(Cow::Borrowed("relay")))
        .filter_map(|tag| {
            if let TagStandard::Relay(url) = tag {
                Some(url.clone())
            } else {
                None
            }
        })
        .take(2)
        .collect();

    for relay in &relays {
        let _ = user.client.add_write_relay(relay).await;
    }

    // Build the poll response
    let builder = EventBuilder::new(Kind::PollResponse, "")
        .tag(Tag::event(event_id))
        .tag(Tag::from_standardized(TagStandard::PollResponse(
            option_id.to_string(),
        )));

    // Send it
    match user.client.send_event_builder_to(relays, builder).await {
        Ok(_) => Response::temporary_redirect(format!("/polls/{event_id_s}")),
        Err(err) => Response::temporary_failure(format!("{err}")),
    }
}