use std::collections::HashMap;
use std::time::Duration;
use crate::aska::WindTemplate;
use crate::nostru;
use crate::rendering;
use crate::routes::response::*;
use crate::user::CaracalUser;
use crate::nips::nip96;
use crate::routes::{
gem, resp_error_send_note, resp_note_prompt_text, resp_redirect_root,
};
use crate::urls::*;
use nostr_sdk::prelude::*;
use askama::Template;
use gnostr_types::{ContentSegment, ShatteredContent};
use hashtag::HashtagParser;
use mdiu::Block;
use rust_i18n::t;
use urlencoding::decode;
use windmark_titanesque::{context::RouteContext, response::Response};
use rand::distributions::{Alphanumeric, DistString};
#[derive(Template)]
#[template(path = "note.gmi", escape = "txt")]
pub struct NoteThreadTemplate {
event: Event,
note_blocks: Vec<Block>,
replies_blocks: Option<Vec<Block>>,
}
#[derive(Template)]
#[template(path = "note_draft_edit.gmi", escape = "txt")]
pub struct NoteDraftEditTemplate<'c> {
url: Url,
draft_id: String,
draft_body: String,
draft_attachments: Vec<&'c str>,
}
pub struct NoteContext {
event: Event,
}
async fn note_context(
params: HashMap<String, String>,
client: &Client,
) -> Option<NoteContext> {
let Some(evid) = params.get("event_id") else {
return None;
};
let Ok(event_id) = EventId::parse(evid) else {
return None;
};
return match client.database().event_by_id(&event_id).await {
Ok(ev) => {
return if ev.is_some() {
Some(NoteContext { event: ev.unwrap() })
} else {
None
};
}
Err(_err) => None,
};
}
fn body_hashtags(body: &str) -> Vec<Tag> {
HashtagParser::new(body)
.map(|ht| Tag::hashtag(&ht.text))
.collect()
}
pub async fn post_note(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let pow: u8 = match ctx.parameters.get("pow") {
Some(value) => match value.to_string().parse::<u8>() {
Ok(int) => int,
Err(_error) => 0,
},
None => 0,
};
let url = ctx.url.clone();
if let Some(text) = dec_urlq(&url) {
let mut tags = Vec::new();
let shattered_content = ShatteredContent::new(text.clone());
for segment in shattered_content.segments.iter() {
if let ContentSegment::NostrUrl(nostr_url) = segment
&& let Ok(n21) = Nip21::parse(&format!("{nostr_url}"))
&& let Nip21::Pubkey(pubk) = n21 {
tags.push(Tag::public_key(pubk));
}
}
let eventb = EventBuilder::text_note(&text).tags(body_hashtags(&text)).tags(tags);
let event = match pow {
5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 => {
eventb.pow(pow).sign(&user.signer).await.unwrap()
}
0 => eventb.sign(&user.signer).await.unwrap(),
_ => todo!(),
};
if let Ok(_event_id) = user.client.send_event(&event).await {
Response::permanent_redirect("/feed")
} else {
Response::temporary_failure("Error")
}
} else {
resp_note_prompt_text()
}
}
pub async fn note_thread(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let mut doc = vec![];
let mut rblocks = vec![];
let evid = ctx.parameters.get("event_id").unwrap();
if let Ok(event_id) = EventId::parse(evid) {
let e_filter = Filter::new().kind(Kind::TextNote).kind(Kind::LongFormTextNote).id(event_id);
let Ok(events) = user
.client
.fetch_combined_events(e_filter, Duration::from_secs(3))
.await else {
return Response::temporary_failure(t!("fetch_events_failed"));
};
let Some(event) = events.first() else {
return Response::temporary_failure(t!("no_such_event"));
};
let rf_filter = Filter::new().kind(Kind::TextNote).event(event_id);
let Ok(replies_events) = user
.client
.fetch_combined_events(rf_filter, Duration::from_secs(5))
.await
else {
return Response::temporary_failure(t!(
"fetch_note_replies_failed"
));
};
let usermeta =
nostru::get_user_metadata(&user.client, event.pubkey).await;
match rendering::render_note(
&event,
usermeta.clone(),
rendering::NoteRenderMode::Normal,
) {
Ok(mut blocks) => {
doc.append(&mut blocks);
}
Err(_err) => {}
}
for revent in replies_events {
let rusermeta =
nostru::get_user_metadata(&user.client, revent.pubkey).await;
if let Ok(mut blks) = rendering::render_note(
&revent,
rusermeta.clone(),
rendering::NoteRenderMode::Normal,
) {
rblocks.append(&mut blks);
}
}
let tmpl = NoteThreadTemplate {
event: event.clone(),
note_blocks: doc,
replies_blocks: Some(rblocks),
};
Response::success(WindTemplate::render(tmpl))
} else {
Response::temporary_failure(t!("invalid_event_id"))
}
}
pub async fn note_reply(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let Some(note_ctx) = note_context(ctx.parameters, &user.client).await
else {
return resp_invalid_params();
};
let url = ctx.url.clone();
if let Some(text) = dec_urlq(&url) {
let event: Event =
EventBuilder::text_note_reply(&text, ¬e_ctx.event, None, None)
.tags(body_hashtags(&text))
.sign(&user.signer)
.await
.unwrap();
let Ok(_) = user.client.send_event(&event).await else {
return resp_error_send_note();
};
Response::success("Sent".to_string())
} else {
resp_note_prompt_text()
}
}
pub async fn note_repost(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let Some(note_ctx) = note_context(ctx.parameters, &user.client).await
else {
return resp_invalid_params();
};
let Ok(event) = EventBuilder::repost(¬e_ctx.event, None)
.sign(&user.signer)
.await
else {
return resp_invalid_params();
};
let Ok(_) = user.client.send_event(&event).await else {
return resp_error_send_note();
};
resp_redirect_root()
}
pub async fn note_react(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let Some(input) = ctx.url.query() else {
return Response::input(t!("input_reaction_emoji"));
};
let Ok(emoji) = decode(input) else {
return Response::input(t!("input_reaction_emoji"));
};
let Some(note_ctx) = note_context(ctx.parameters, &user.client).await
else {
return resp_invalid_params();
};
let Ok(event) = EventBuilder::reaction(¬e_ctx.event, emoji)
.sign(&user.signer)
.await
else {
return resp_invalid_params();
};
let Ok(_) = user.client.send_event(&event).await else {
return resp_error_send_note();
};
resp_redirect_root()
}
pub async fn draft_new(
ctx: RouteContext,
_user: &'static mut CaracalUser,
) -> Response {
let id = Alphanumeric
.sample_string(&mut rand::thread_rng(), 16)
.to_string();
Response::temporary_redirect(titan_url(
ctx.url,
&format!("/draft/{}/edit", id),
))
}
pub async fn draft_edit(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let page_url = ctx.url.clone();
let Some(draft_id) = ctx.parameters.get("draft_id") else {
return resp_invalid_params();
};
if let Some(titan) = ctx.titan_rsc {
let Ok(rsc_text) = titan.content_text() else {
return resp_error_generic();
};
let rsc_text = rsc_text
.lines()
.filter(|l| !l.starts_with("=> titan://"))
.filter(|l| !l.starts_with("=> gemini://"))
.filter(|l| !l.is_empty())
.collect::<Vec<&str>>()
.join("\n");
let _ = user
.storage
.draft_store(&draft_id.clone(), rsc_text.as_str());
}
let Ok(mut rtxn) = user.storage.get_read_txn() else {
return resp_invalid_params();
};
let Ok(draft) = user.storage.draft_get(&draft_id.clone(), &mut rtxn) else {
return resp_error_generic();
};
let template = NoteDraftEditTemplate {
url: page_url,
draft_id: draft_id.clone(),
draft_body: draft.content.to_string(),
draft_attachments: draft.attachments,
};
Response::success(WindTemplate::render(template))
}
pub async fn draft_post(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let Some(draft_id) = ctx.parameters.get("draft_id") else {
return resp_invalid_params();
};
let Ok(mut rtxn) = user.storage.get_read_txn() else {
return resp_invalid_params();
};
let Ok(draft) = user.storage.draft_get(&draft_id.clone(), &mut rtxn) else {
return resp_error_generic();
};
let body = draft.render_text();
let event: Event = EventBuilder::text_note(&body)
.tags(body_hashtags(&body))
.sign(&user.signer)
.await
.unwrap();
let Ok(_) = user.client.send_event(&event).await else {
return resp_error_send_note();
};
resp_redirect_root()
}
pub async fn draft_upload_file(
ctx: RouteContext,
user: &'static mut CaracalUser,
) -> Response {
let Some(draft_id) = ctx.parameters.get("draft_id") else {
return resp_invalid_params();
};
let Some(titan_file) = ctx.titan_rsc else {
return Response::temporary_failure("No file");
};
match nip96::upload_file_data(
None,
titan_file.content,
titan_file.mime,
&user.upload_keys,
)
.await
{
Ok(file_url) => {
let Ok(mut rtxn) = user.storage.get_read_txn() else {
return resp_invalid_params();
};
let _ = user
.storage
.draft_attach(&mut rtxn, &draft_id.clone(), file_url)
.unwrap();
Response::temporary_redirect(format!(
"gemini://{}/draft/{}/edit",
ctx.url.authority(),
draft_id
))
}
Err(_error) => Response::temporary_failure("no file"),
}
}