use std::error::Error;
use std::str::FromStr;
use http::Uri;
use gnostr_types::{ContentSegment, ShatteredContent};
use mdiu::{Block, Preformatted, Content, Level, Link};
use nostr_sdk::prelude::*;
use regex::Regex;
use urlencoding::encode;
use gemini::{Doc, gemtext::Level as GtLevel};
use crate::nostru;
use crate::ts;
use rust_i18n::t;
rust_i18n::i18n!("locales");
pub fn h1(text: String) -> Block {
Block::Heading(Level::One, Content::new(text).unwrap())
}
pub fn h2(text: String) -> Block {
Block::Heading(Level::Two, Content::new(text).unwrap())
}
pub fn h3(text: String) -> Block {
Block::Heading(Level::Three, Content::new(text).unwrap())
}
pub fn lnk(uri: Uri, opt_label: Option<String>) -> Block {
if let Some(text) = opt_label {
Block::Link(Link::new(uri, Some(Content::new(text).unwrap())))
} else {
Block::Link(Link::new(uri, None))
}
}
pub fn blockify(gemtext: String) -> Vec<Block> {
let mut blocks = Vec::new();
if let Ok(builder) = gemini::parse::parse_gemtext(gemtext) {
for element in builder {
let block = match element {
Doc::Text(text) => {
let Ok(content) = Content::new(text) else { continue };
Block::Text(content)
}
Doc::Preformatted { alt, text } => {
Block::Preformatted(
Preformatted::new(text, alt.and_then(|atext| Content::new(atext).ok())))
}
Doc::Heading(level, text) => {
let lvl = match level {
GtLevel::One => Level::One,
GtLevel::Two => Level::Two,
GtLevel::Three => Level::Three
};
let Ok(content) = Content::new(text) else { continue };
Block::Heading(lvl, content)
}
Doc::Quote(quote) => {
let Ok(content) = Content::new(quote) else { continue };
Block::Quote(content)
}
Doc::Link { to, name } => {
let Ok(uri) = Uri::from_str(&to) else { continue };
Block::Link(
Link::new(uri, name.and_then(|n| Content::new(n).ok()))
)
}
Doc::ListItem(text) => {
let Ok(content) = Content::new(text) else { continue };
Block::ListItem(content)
}
Doc::Blank => {
Block::Empty
}
};
blocks.push(block);
}
}
blocks
}
pub fn geminize_note(event: &Event) -> Vec<(Block, Option<Uri>)> {
let mut vec: Vec<(Block, Option<Uri>)> = Vec::new();
let mut links: Vec<(String, Uri)> = Vec::new();
let ht_re = Regex::new(r"^#").unwrap();
let shattered_content = ShatteredContent::new(event.content.clone());
for segment in shattered_content.segments.iter() {
match segment {
ContentSegment::NostrUrl(nostr_url) => {
let Ok(n21) = Nip21::parse(&format!("{nostr_url}")) else {
eprintln!("Invalid nip21 url: {nostr_url}");
continue;
};
match n21 {
Nip21::EventId(event_id) => {
let bech32 = event_id.to_bech32().unwrap();
let Ok(uri) = Uri::from_str(&format!(
"/note/{}/thread",
bech32
)) else {
continue;
};
links.push((t!("read_ref_note_with_bech", event_bech = bech32).to_string(), uri));
}
Nip21::Event(event) => {
let bech32 = event.event_id.to_bech32().unwrap();
let Ok(uri) = Uri::from_str(&format!(
"/note/{}/thread",
bech32
)) else {
continue;
};
links.push((t!("read_ref_note_with_bech", event_bech = bech32).to_string(), uri));
}
Nip21::Pubkey(pubkey) => {
let Ok(uri) =
Uri::from_str(&format!("/p/{}", pubkey.to_hex()))
else {
continue;
};
links.push((pubkey.to_bech32().unwrap(), uri));
}
Nip21::Profile(profile) => {
let Ok(uri) = Uri::from_str(&format!(
"/p/{}",
profile.public_key.to_hex()
)) else {
continue;
};
links.push((
profile.public_key.to_bech32().unwrap(),
uri,
));
}
_ => (),
}
}
ContentSegment::Hyperlink(span) => {
let Some(chunk) = shattered_content.slice(span) else {
continue;
};
let Ok(url) = Url::parse(chunk) else {
continue;
};
let scheme = url.scheme();
let Ok(orig_uri) = Uri::from_str(url.as_str()) else {
continue;
};
match scheme {
"ws" | "wss" | "ftp" | "http" | "https" | "gemini" => {
links.push((
orig_uri.clone().to_string(),
orig_uri.clone(),
));
}
scheme => println!("Unhandled URL scheme {scheme}"),
}
}
ContentSegment::Plain(span) => {
let Some(chunk) = shattered_content.slice(span) else {
continue;
};
for line in chunk.lines() {
let cline = ht_re.replace(line, "");
let Ok(content) = Content::new(cline) else {
continue;
};
vec.push((Block::Text(content), None));
}
}
_ => (),
}
for (label, link) in &links {
vec.push((lnk(link.clone(), Some(label.clone())), None));
}
links.clear();
}
vec
}
#[derive(Copy, Clone, PartialEq)]
pub enum NoteRenderMode {
Tiny,
Minimal,
Normal,
Verbose,
}
pub fn render_note(
event: &Event,
metadata: Option<Metadata>,
mode: NoteRenderMode,
) -> Result<Vec<Block>, Box<dyn Error>> {
let mut blocks = vec![];
let mut p_name: String = String::new();
let mut header = String::new();
let ts_diff = Timestamp::now() - event.created_at;
let ts_ago = ts::time_ago(ts_diff.as_u64());
if metadata.is_some() {
let meta = metadata.unwrap();
match meta.name {
Some(ref name) => {
header.push_str(name.as_str());
p_name = name.clone();
}
None => header.push('?'),
}
if let Some(ref dname) = meta.display_name {
header.push_str(format!(" ({})", dname).as_str())
}
} else {
header = event.id.to_hex();
}
header.push_str(format!(" ({} ago)", ts_ago).as_str());
blocks.push(h3(header));
match event.kind {
Kind::TextNote => {
for (block, _uri) in geminize_note(event) {
blocks.push(block);
}
}
Kind::LongFormTextNote => {
blocks.extend(blockify(md2gemtext::convert(&event.content)));
}
_ => ()
}
if mode != NoteRenderMode::Tiny {
for htag in event.tags.hashtags() {
if let Ok(uri) = Uri::builder()
.path_and_query(format!("/hashtags/{}/notes", encode(htag)))
.build()
{
blocks
.push(lnk(uri, Some(format!("Tag: {}", htag).to_string())));
}
}
}
let prouri = Uri::builder()
.path_and_query(format!("/p/{}", event.pubkey.to_bech32()?))
.build()?;
let thread_uri = Uri::builder()
.path_and_query(format!("/note/{}/thread", event.id.to_hex()))
.build()?;
let reply_uri = Uri::builder()
.path_and_query(format!("/note/{}/reply", event.id.to_hex()))
.build()?;
let repost_uri = Uri::builder()
.path_and_query(format!("/note/{}/repost", event.id.to_hex()))
.build()?;
let react_uri = Uri::builder()
.path_and_query(format!("/note/{}/react", event.id.to_hex()))
.build()?;
blocks.push(Block::Link(Link::new(
prouri,
Some(Content::new(format!("{} (profile)", p_name)).unwrap()),
)));
blocks.push(lnk(thread_uri, Some(t!("thread").into())));
blocks.push(lnk(reply_uri, Some(t!("reply").into())));
blocks.push(lnk(repost_uri, Some(t!("repost").into())));
blocks.push(lnk(react_uri, Some(t!("react").into())));
Ok(blocks)
}
pub async fn render_notes(
events: Events,
client: &Client,
idx_start: Option<usize>,
idx_end: Option<usize>,
mode: NoteRenderMode,
) -> Result<Vec<Block>, Box<dyn Error>> {
let mut gemb = vec![];
let evcount = events.len();
let start = idx_start.unwrap_or_default();
let end = match idx_end {
Some(idx) => {
if evcount < idx {
evcount
} else {
idx
}
}
None => evcount,
};
for event in &events.to_vec()[start..end] {
let usermeta = nostru::get_user_metadata(client, event.pubkey).await;
match render_note(event, usermeta, mode) {
Ok(mut blocks) => {
gemb.append(&mut blocks);
}
Err(_err) => {
gemb.push(Block::Text(
Content::new("Cannot process note").unwrap(),
));
}
}
}
Ok(gemb)
}