mod bookmarks;
mod community;
mod contacts;
mod dm;
mod filters;
mod following;
mod hashtags;
mod inbox;
mod manual;
mod mute;
mod nostr_db;
mod notes;
mod nsite;
mod outbox;
mod polls;
mod profile;
mod remote_signer;
mod response;
mod search;
mod sec;
mod settings;
mod www;
mod route_prelude;
pub use response::*;
pub use bookmarks::{
bookmark_public_add, public_bookmarks, public_bookmarks_delete_event,
};
pub use community::{
nostriches_list_all, nostriches_list_recent, nostriches_search,
};
pub use contacts::contact_list_index;
pub use dm::{dm_all, 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 manual::manual_root;
pub use mute::{person_mute, person_unmute};
pub use nostr_db::db_purge;
pub use notes::{
draft_body_append, draft_edit, draft_longform_body_append,
draft_longform_delete_last_line, draft_longform_edit, draft_longform_new,
draft_longform_new_with_id, draft_longform_post, draft_longform_preview,
draft_longform_set, draft_new, draft_post, draft_upload_file, note_delete,
note_react_custom, note_react_select, note_reply, note_repost, note_thread,
post_note,
};
pub use nsite::nsites_index;
pub use outbox::outbox;
pub use polls::{poll_details, poll_respond, polls_list};
pub use profile::{
profile_edit, profile_index, profile_init_reset, profile_titan_edit,
profile_upload,
};
pub use remote_signer::{
nostr_connect_bunker, nostr_connect_client_initiated, nostr_connect_index,
};
pub use search::search_content;
pub use settings::{
relay_modify, relays_add_from_popular, relays_settings, settings_index,
ui_settings, ui_settings_chlocale,
};
pub use www::route_www_get;
use crate::aska::WindTemplate;
use crate::emoji::*;
use crate::nostru;
use crate::rendering;
use crate::user::{CaracalUser, lookup_user, lookup_user_mut};
use crate::util::fingerprint;
use urlencoding::{decode, encode};
use askama::Template;
use colored::*;
use openssl::nid::Nid;
use windmark_titanesque::{context::RouteContext, response::Response};
use chrono::DateTime;
use nostr_sdk::prelude::*;
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)]
#[template(path = "index.gmi", escape = "txt")]
struct IndexTemplate<'a> {
connect_uri: Option<&'a NostrConnectURI>,
connect_relays: Option<Vec<String>>,
profile_metadata: Option<Metadata>,
}
#[derive(Template, Clone)]
#[template(path = "index_nocert.gmi", escape = "txt")]
struct IndexNoCertTemplate {}
#[derive(Template, Clone)]
#[template(path = "about.gmi", escape = "txt")]
struct AboutTemplate {}
#[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>>,
payment_targets: Option<Event>,
}
pub 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(
"Could not load your certificate",
);
};
println!("{} {}", "From:".red(), fingerprint.yellow());
println!("{} {}", "=>".yellow(), ctx.url.path().blue());
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()
&& !ctx.url.path().starts_with("/connect")
{
nostr_connect_index(ctx, user).await
} else {
handler(ctx, user).await
}
} else {
handler(ctx, user).await
}
} else {
let resp = handler(ctx, user).await;
match resp.status {
10 => println!("{}", resp.status.to_string().blue()),
20..=22 => println!("{}", resp.status.to_string().green()),
_ => println!("{}", resp.status.to_string().red()),
}
resp
}
} 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 about(_ctx: RouteContext) -> Response {
Response::success(WindTemplate::render(AboutTemplate {}))
}
pub async fn index(
_ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let Ok(signer) = user.client.signer().await else {
return Response::temporary_failure("Signer error!");
};
match signer.get_public_key().await {
Ok(pubkey) => {
let profile_metadata =
nostru::get_user_metadata(&user.client, pubkey).await;
let connect_relays = user.connect_uri.as_ref().map(|uri| {
uri.relays().iter().map(|r| r.to_string()).collect()
});
Response::success(WindTemplate::render(IndexTemplate {
profile_metadata,
connect_uri: user.connect_uri.as_ref(),
connect_relays,
}))
}
Err(_) => {
Response::temporary_redirect("/connect")
}
}
}
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(mut contacts) = user.contact_list_pubkeys().await else {
return Response::temporary_failure("Failed to fetch contact list");
};
let mute_list = user.mute_list().await;
contacts.retain(|pk| !mute_list.public_keys.contains(pk));
let filter = Filter::new()
.kind(Kind::TextNote)
.kind(Kind::LongFormTextNote)
.authors(contacts)
.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;
match rendering::render_note(
&user.client,
&event,
usermeta,
rendering::NoteRenderOptions::default()
| rendering::NoteRenderOptions::SHOW_HASHTAGS,
)
.await
{
Ok(mut blocks) => {
doc.append(&mut blocks);
}
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();
let Some(npub) = params.get("npub") else {
return resp_invalid_params();
};
let Ok(pubkey) = PublicKey::parse(npub) else {
return resp_invalid_params();
};
let usermeta = nostru::get_user_metadata(&user.client, pubkey).await;
let _ = user
.client
.subscribe(nostru::notes_filter().author(pubkey).limit(500), None)
.await;
let Ok(pt_events) = user
.fetch_quick(
Filter::new()
.kind(Kind::Custom(10133))
.author(pubkey)
.limit(1),
)
.await
else {
return Response::temporary_failure("Could not fetch payment targets");
};
let payment_targets = pt_events.first_owned();
let notes: Option<Vec<Block>> = match user
.client
.database()
.query(nostru::notes_filter().author(pubkey).limit(300))
.await
{
Ok(events) => (rendering::render_notes(
events,
&user.client,
None,
None,
rendering::NoteRenderOptions::default(),
)
.await)
.ok(),
Err(_error) => None,
};
let template = PersonIndexTemplate {
npub: pubkey.to_bech32().unwrap(),
metadata: usermeta,
notes_blocks: notes,
payment_targets,
};
Response::success(WindTemplate::render(template))
}