mod contacts;
mod dm;
mod following;
mod hashtags;
mod inbox;
mod notes;
mod nsite;
mod polls;
mod profile;
mod remote_signer;
mod response;
mod search;
pub use response::*;
pub use contacts::contact_list_index;
pub use dm::{person_dm_conversation, person_send_dm};
pub use following::{follow, person_follow, person_unfollow};
pub use hashtags::{hashtags_index, notes_by_hashtag};
pub use inbox::inbox;
pub use notes::{
draft_edit, draft_new, draft_post, draft_upload_file, note_react,
note_reply, note_repost, note_thread, post_note,
};
pub use nsite::nsites_index;
pub use polls::{poll_details, poll_respond, polls_list};
pub use profile::{
profile_edit, profile_index, profile_init_reset, profile_upload,
};
pub use remote_signer::{nostr_connect_change, nostr_connect_index};
pub use search::search_content;
use crate::aska::WindTemplate;
use crate::nostru;
use crate::rendering;
use crate::user::{CaracalUser, lookup_user, lookup_user_mut};
use crate::util::fingerprint;
use askama::Template;
use openssl::nid::Nid;
use windmark_titanesque::{context::RouteContext, response::Response};
use chrono::DateTime;
use nostr_sdk::prelude::*;
use std::time::Duration;
use mdiu::Gemtext;
use mdiu::{Block, Content, Level, Markup};
use openssl::x509::X509;
use rust_i18n::t;
use std::future::IntoFuture;
rust_i18n::i18n!("locales");
#[derive(Template, Clone, Copy)]
#[template(path = "index.gmi", escape = "txt")]
struct IndexTemplate {}
#[derive(Template, Clone, Copy)]
#[template(path = "index_nocert.gmi", escape = "txt")]
struct IndexNoCertTemplate {}
#[derive(Template)]
#[template(path = "feed.gmi", escape = "txt")]
pub struct FeedTemplate {
b1: Block,
edoc: Vec<Block>,
}
#[derive(Template)]
#[template(path = "person_index.gmi", escape = "txt")]
pub struct PersonIndexTemplate {
npub: String,
metadata: Option<Metadata>,
notes_blocks: Option<Vec<Block>>,
}
#[derive(Template)]
#[template(path = "profile_index.gmi", escape = "txt")]
pub struct MyProfileTemplate {
url: Url,
npub: String,
metadata: Option<Metadata>,
}
fn gem(blocks: &[Block]) -> String {
<Gemtext>::markup(blocks)
}
pub fn humanize_ts(ts: &Timestamp) -> String {
if let Ok(dt) =
DateTime::parse_from_rfc3339(ts.to_human_datetime().as_str())
{
format!("{}", dt.format("%a %b %e %Y, %T"))
} else {
String::from("Invalid date")
}
}
pub async fn guardian<R>(
ctx: RouteContext,
mut handler: impl FnMut(RouteContext, &'static mut CaracalUser) -> R
+ Send
+ Sync
+ 'static,
) -> Response
where
R: IntoFuture<Output = Response> + Send + 'static,
{
if let Some(cert) = &ctx.certificate {
let Some(fingerprint) = fingerprint(cert) else {
return Response::temporary_failure("Cannot get fingerprint");
};
let Some(user) = lookup_user_mut(fingerprint) else {
return Response::temporary_failure("Cannot find user");
};
let subject = cert.subject_name();
if let Some(org) = subject.entries_by_nid(Nid::ORGANIZATIONNAME).next()
{
let Ok(org) = org.data().as_utf8() else {
return Response::temporary_failure(
"Failed to retrieve organization field in cert",
);
};
if org.to_string().to_lowercase() == "nostr" {
if user.connect_uri.is_none() {
nostr_connect_change(ctx).await
} else {
handler(ctx, user).await
}
} else {
handler(ctx, user).await
}
} else {
handler(ctx, user).await
}
} else {
Response::success(WindTemplate::render(IndexNoCertTemplate {}))
}
}
pub async fn ugate(certificate: Option<X509>) -> Option<&'static CaracalUser> {
if let Some(cert) = certificate {
match fingerprint(&cert) {
Some(fp) => lookup_user(fp),
None => None,
}
} else {
None
}
}
pub async fn ugate_mut(
certificate: Option<X509>,
) -> Option<&'static mut CaracalUser> {
if let Some(cert) = certificate {
match fingerprint(&cert) {
Some(fp) => lookup_user_mut(fp),
None => None,
}
} else {
None
}
}
pub async fn ugate_m(ctx: &RouteContext) -> Option<&'static mut CaracalUser> {
match &ctx.certificate {
Some(cert) => match fingerprint(cert) {
Some(fp) => lookup_user_mut(fp),
None => None,
},
None => None,
}
}
pub async fn index(
ctx: RouteContext,
_user: &'static mut CaracalUser,
) -> Response {
if let Some(_) = ugate(ctx.certificate).await {
Response::success(WindTemplate::render(IndexTemplate {}))
} else {
Response::success(WindTemplate::render(IndexNoCertTemplate {}))
}
}
pub async fn feed(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let params = ctx.parameters.clone();
let default = String::from("500");
let limit_str = params.get("limit").unwrap_or(&default);
let limit = match limit_str.parse::<usize>() {
Ok(result) => result,
Err(_error) => default.parse::<usize>().unwrap(),
};
let Ok(contacts) =
user.client.get_contact_list(Duration::from_secs(10)).await
else {
return Response::temporary_failure("Failed to fetch contact list");
};
let filter = Filter::new()
.kind(Kind::TextNote)
.kind(Kind::LongFormTextNote)
.authors(contacts.iter().map(|c| c.public_key))
.limit(limit);
let Ok(events) = user.client.database().query(filter).await else {
return Response::temporary_failure("db error");
};
let mut doc = vec![];
for event in events.into_iter() {
let usermeta =
nostru::get_user_metadata(&user.client, event.pubkey).await;
if let Err(err) = user
.client
.subscribe(Filter::new().kind(Kind::Reaction).event(event.id), None)
.await
{
eprintln!("Reactions sub failed: {err}");
}
let rfilter = Filter::new().kind(Kind::Reaction).event(event.id);
let Ok(reactions) = user.client.database().query(rfilter).await else {
continue;
};
match rendering::render_note(
&event,
usermeta,
rendering::NoteRenderMode::Normal,
) {
Ok(mut blocks) => {
doc.append(&mut blocks);
let rsummary = nostru::reactions_summary(&reactions);
if !rsummary.is_empty() {
doc.push(Block::Text(Content::new(rsummary).unwrap()));
}
}
Err(_err) => {
doc.push(Block::Text(
Content::new("Cannot process note").unwrap(),
));
}
}
}
let tmpl = FeedTemplate {
edoc: doc,
b1: Block::Heading(Level::One, "caracal!".parse().unwrap()),
};
Response::success(WindTemplate::render(tmpl))
}
pub async fn person_profile(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let params = ctx.parameters.clone();
if let Some(npub) = params.get("npub") {
let Ok(pubkey) = PublicKey::parse(npub) else {
return resp_invalid_params();
};
let usermeta = nostru::get_user_metadata(&user.client, pubkey).await;
let filter = nostru::notes_filter(500).author(pubkey);
let notes: Option<Vec<Block>> =
match user.client.database().query(filter).await {
Ok(events) => {
(rendering::render_notes(
events,
&user.client,
None,
None,
rendering::NoteRenderMode::Normal,
)
.await).ok()
}
Err(_error) => None,
};
let template = PersonIndexTemplate {
npub: npub.to_string(),
metadata: usermeta,
notes_blocks: notes,
};
Response::success(WindTemplate::render(template))
} else {
resp_invalid_params()
}
}