tetratto/crates/app/src/assets.rs

438 lines
20 KiB
Rust
Raw Normal View History

2025-05-31 13:07:34 -04:00
use bberry::{
core::element::{Element, Render},
text, read_param,
};
use pathbufd::PathBufD;
use regex::Regex;
use std::{
collections::HashMap,
fs::{exists, read_to_string, write},
sync::LazyLock,
};
use tera::Context;
use tetratto_core::{
config::Config,
2025-05-05 19:38:01 -04:00
model::{
auth::{DefaultTimelineChoice, User},
permissions::FinePermission,
},
PUBSUB_ENABLED,
};
use tetratto_l10n::LangFile;
2025-03-23 16:37:43 -04:00
use tetratto_shared::hash::salt;
use tokio::sync::RwLock;
use crate::{create_dir_if_not_exists, write_if_track, write_template};
// images
pub const DEFAULT_AVATAR: &str = include_str!("./public/images/default-avatar.svg");
pub const DEFAULT_BANNER: &str = include_str!("./public/images/default-banner.svg");
pub const FAVICON: &str = include_str!("./public/images/favicon.svg");
// css
pub const STYLE_CSS: &str = include_str!("./public/css/style.css");
// js
pub const LOADER_JS: &str = include_str!("./public/js/loader.js");
2025-03-23 16:37:43 -04:00
pub const ATTO_JS: &str = include_str!("./public/js/atto.js");
pub const ME_JS: &str = include_str!("./public/js/me.js");
pub const STREAMS_JS: &str = include_str!("./public/js/streams.js");
// html
2025-05-31 10:17:49 -04:00
pub const ROOT: &str = include_str!("./public/html/root.lisp");
pub const MACROS: &str = include_str!("./public/html/macros.html");
pub const COMPONENTS: &str = include_str!("./public/html/components.html");
2025-05-30 21:22:53 -04:00
pub const MISC_ERROR: &str = include_str!("./public/html/misc/error.lisp");
2025-03-30 22:26:20 -04:00
pub const MISC_NOTIFICATIONS: &str = include_str!("./public/html/misc/notifications.html");
2025-05-30 21:22:53 -04:00
pub const MISC_MARKDOWN: &str = include_str!("./public/html/misc/markdown.lisp");
2025-04-12 22:25:54 -04:00
pub const MISC_REQUESTS: &str = include_str!("./public/html/misc/requests.html");
pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.html");
pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.html");
pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.html");
pub const AUTH_CONNECTION: &str = include_str!("./public/html/auth/connection.html");
pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.html");
pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.html");
2025-03-31 11:45:34 -04:00
pub const PROFILE_SETTINGS: &str = include_str!("./public/html/profile/settings.html");
2025-03-31 22:35:11 -04:00
pub const PROFILE_FOLLOWING: &str = include_str!("./public/html/profile/following.html");
pub const PROFILE_FOLLOWERS: &str = include_str!("./public/html/profile/followers.html");
pub const PROFILE_WARNING: &str = include_str!("./public/html/profile/warning.html");
pub const PROFILE_PRIVATE: &str = include_str!("./public/html/profile/private.html");
pub const PROFILE_BLOCKED: &str = include_str!("./public/html/profile/blocked.html");
pub const PROFILE_BANNED: &str = include_str!("./public/html/profile/banned.html");
pub const PROFILE_REPLIES: &str = include_str!("./public/html/profile/replies.html");
pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.html");
pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.html");
pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.html");
pub const COMMUNITIES_FEED: &str = include_str!("./public/html/communities/feed.html");
2025-03-31 11:45:34 -04:00
pub const COMMUNITIES_SETTINGS: &str = include_str!("./public/html/communities/settings.html");
2025-04-03 20:05:21 -04:00
pub const COMMUNITIES_MEMBERS: &str = include_str!("./public/html/communities/members.html");
2025-04-10 21:37:33 -04:00
pub const COMMUNITIES_SEARCH: &str = include_str!("./public/html/communities/search.html");
pub const COMMUNITIES_CREATE_POST: &str =
include_str!("./public/html/communities/create_post.html");
2025-04-12 22:25:54 -04:00
pub const COMMUNITIES_QUESTION: &str = include_str!("./public/html/communities/question.html");
pub const COMMUNITIES_QUESTIONS: &str = include_str!("./public/html/communities/questions.html");
pub const POST_POST: &str = include_str!("./public/html/post/post.html");
pub const POST_REPOSTS: &str = include_str!("./public/html/post/reposts.html");
pub const POST_QUOTES: &str = include_str!("./public/html/post/quotes.html");
pub const POST_LIKES: &str = include_str!("./public/html/post/likes.html");
2025-03-31 19:31:36 -04:00
pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.html");
pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.html");
pub const TIMELINES_FOLLOWING: &str = include_str!("./public/html/timelines/following.html");
pub const TIMELINES_ALL: &str = include_str!("./public/html/timelines/all.html");
2025-04-13 12:15:14 -04:00
pub const TIMELINES_HOME_QUESTIONS: &str =
include_str!("./public/html/timelines/home_questions.html");
2025-04-13 12:58:44 -04:00
pub const TIMELINES_POPULAR_QUESTIONS: &str =
include_str!("./public/html/timelines/popular_questions.html");
2025-04-13 12:15:14 -04:00
pub const TIMELINES_FOLLOWING_QUESTIONS: &str =
include_str!("./public/html/timelines/following_questions.html");
pub const TIMELINES_ALL_QUESTIONS: &str =
include_str!("./public/html/timelines/all_questions.html");
2025-05-18 16:43:56 -04:00
pub const TIMELINES_SEARCH: &str = include_str!("./public/html/timelines/search.html");
2025-03-31 19:31:36 -04:00
pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.html");
pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.html");
pub const MOD_FILE_REPORT: &str = include_str!("./public/html/mod/file_report.html");
pub const MOD_IP_BANS: &str = include_str!("./public/html/mod/ip_bans.html");
2025-04-06 13:43:12 -04:00
pub const MOD_PROFILE: &str = include_str!("./public/html/mod/profile.html");
2025-04-11 22:12:43 -04:00
pub const MOD_WARNINGS: &str = include_str!("./public/html/mod/warnings.html");
2025-05-02 20:51:19 -04:00
pub const MOD_STATS: &str = include_str!("./public/html/mod/stats.html");
2025-04-27 23:11:37 -04:00
pub const CHATS_APP: &str = include_str!("./public/html/chats/app.html");
pub const CHATS_STREAM: &str = include_str!("./public/html/chats/stream.html");
pub const CHATS_MESSAGE: &str = include_str!("./public/html/chats/message.html");
pub const CHATS_CHANNELS: &str = include_str!("./public/html/chats/channels.html");
2025-04-27 23:11:37 -04:00
2025-05-08 22:18:04 -04:00
pub const STACKS_LIST: &str = include_str!("./public/html/stacks/list.html");
pub const STACKS_POSTS: &str = include_str!("./public/html/stacks/posts.html");
pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.html");
// langs
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
// ...
pub const VENDOR_SPOTIFY_ICON: &str = include_str!("./public/images/vendor/spotify.svg");
2025-04-26 19:23:30 -04:00
pub const VENDOR_LAST_FM_ICON: &str = include_str!("./public/images/vendor/last-fm.svg");
2025-05-03 21:18:07 -04:00
pub(crate) static HTML_FOOTER: LazyLock<RwLock<String>> =
LazyLock::new(|| RwLock::new(String::new()));
/// A container for all loaded icons.
pub(crate) static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
/// Pull an icon given its name and insert it into [`ICONS`].
pub(crate) async fn pull_icon(icon: &str, icons_dir: &str) {
let writer = &mut ICONS.write().await;
let icon_url = format!(
"https://raw.githubusercontent.com/lucide-icons/lucide/refs/heads/main/icons/{icon}.svg"
);
let file_path = PathBufD::current().extend(&[icons_dir, icon]);
if exists(&file_path).unwrap() {
writer.insert(icon.to_string(), read_to_string(&file_path).unwrap());
return;
}
println!("download icon: {icon}");
2025-05-31 10:17:49 -04:00
let svg = reqwest::get(icon_url)
.await
.unwrap()
.text()
.await
.unwrap()
.replace("\n", "");
write(&file_path, &svg).unwrap();
writer.insert(icon.to_string(), svg);
}
macro_rules! vendor_icon {
($name:literal, $icon:ident, $icons_dir:expr) => {{
let writer = &mut ICONS.write().await;
writer.insert($name.to_string(), $icon.to_string());
let file_path = PathBufD::current().extend(&[$icons_dir.clone(), $name.to_string()]);
std::fs::write(file_path, $icon).unwrap();
}};
}
/// Read a string and replace all custom blocks with the corresponding correct HTML.
///
/// # Replaces
/// * icons
/// * icons (with class specifier)
/// * l10n text
2025-05-31 13:07:34 -04:00
pub(crate) async fn replace_in_html(
input: &str,
config: &Config,
lisp: bool,
plugins: Option<&mut HashMap<String, Box<dyn FnMut(Element) -> Element>>>,
) -> String {
2025-05-03 21:18:07 -04:00
let reader = HTML_FOOTER.read().await;
if reader.is_empty() {
if !config.html_footer_path.is_empty() {
drop(reader);
let mut writer = HTML_FOOTER.write().await;
*writer = read_to_string(PathBufD::current().join(&config.html_footer_path)).unwrap();
} else {
drop(reader);
}
} else {
drop(reader);
}
let reader = HTML_FOOTER.read().await;
// ...
2025-05-30 21:22:53 -04:00
let mut input = if !lisp {
input.to_string()
} else {
2025-05-31 13:07:34 -04:00
if let Some(plugins) = plugins {
bberry::parse(input).render(plugins)
} else {
bberry::parse(input).render_safe()
}
2025-05-30 21:22:53 -04:00
};
input = input.replace("<!-- prettier-ignore -->", "");
// l10n text
let text = Regex::new("(\\{\\{)\\s*(text)\\s*\"(.*?)\"\\s*(\\}\\})").unwrap();
for cap in text.captures_iter(&input.clone()) {
let replace_with = format!("{{{{ lang[\"{}\"] }}}}", cap.get(3).unwrap().as_str());
input = input.replace(cap.get(0).unwrap().as_str(), &replace_with);
}
// icon (with class)
let icon_with_class =
Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap();
for cap in icon_with_class.captures_iter(&input.clone()) {
let icon = &cap.get(3).unwrap().as_str().replace("\"", "");
pull_icon(icon, &config.dirs.icons).await;
let reader = ICONS.read().await;
let icon_text = reader.get(icon).unwrap().replace(
"<svg",
&format!("<svg class=\"icon {}\"", cap.get(4).unwrap().as_str()),
);
input = input.replace(cap.get(0).unwrap().as_str(), &icon_text);
}
// icon (without class)
let icon_without_class = Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*(\\}\\})").unwrap();
for cap in icon_without_class.captures_iter(&input.clone()) {
let icon = &cap.get(3).unwrap().as_str().replace("\"", "");
pull_icon(icon, &config.dirs.icons).await;
let reader = ICONS.read().await;
let icon_text = reader
.get(icon)
.unwrap()
.replace("<svg", "<svg class=\"icon\"");
input = input.replace(cap.get(0).unwrap().as_str(), &icon_text);
}
// return
2025-05-03 21:18:07 -04:00
input = input.replacen("<!-- html_footer_goes_here -->", &format!("{reader}"), 1);
input
}
2025-05-31 13:07:34 -04:00
pub(crate) fn lisp_plugins() -> HashMap<String, Box<dyn FnMut(Element) -> Element>> {
let mut plugins = HashMap::new();
plugins.insert(
"icon".to_string(),
Box::new(|e: Element| text!(format!("{{{{ icon \"{}\" }}}}", read_param!(e, 0)))) as _,
);
plugins.insert(
"icon_class".to_string(),
Box::new(|e: Element| {
text!(format!(
"{{{{ icon \"{}\" c({}) }}}}",
read_param!(e, 0),
read_param!(e, 1)
))
}) as _,
);
plugins.insert(
"str".to_string(),
Box::new(|e: Element| text!(format!("{{{{ text \"{}\" }}}}", read_param!(e, 0)))) as _,
);
plugins
}
/// Set up public directories.
pub(crate) async fn write_assets(config: &Config) -> PathBufD {
vendor_icon!("spotify", VENDOR_SPOTIFY_ICON, config.dirs.icons);
2025-04-26 19:23:30 -04:00
vendor_icon!("last_fm", VENDOR_LAST_FM_ICON, config.dirs.icons);
// ...
2025-05-31 13:07:34 -04:00
let mut plugins = lisp_plugins();
let html_path = PathBufD::current().join(&config.dirs.templates);
2025-05-31 13:07:34 -04:00
write_template!(html_path->"root.html"(crate::assets::ROOT) --config=config --lisp plugins);
write_template!(html_path->"macros.html"(crate::assets::MACROS) --config=config);
write_template!(html_path->"components.html"(crate::assets::COMPONENTS) --config=config);
2025-05-31 13:07:34 -04:00
write_template!(html_path->"misc/error.html"(crate::assets::MISC_ERROR) -d "misc" --config=config --lisp plugins);
2025-03-30 22:26:20 -04:00
write_template!(html_path->"misc/notifications.html"(crate::assets::MISC_NOTIFICATIONS) --config=config);
2025-05-31 13:07:34 -04:00
write_template!(html_path->"misc/markdown.html"(crate::assets::MISC_MARKDOWN) --config=config --lisp plugins);
2025-04-12 22:25:54 -04:00
write_template!(html_path->"misc/requests.html"(crate::assets::MISC_REQUESTS) --config=config);
write_template!(html_path->"auth/base.html"(crate::assets::AUTH_BASE) -d "auth" --config=config);
write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config);
write_template!(html_path->"auth/register.html"(crate::assets::AUTH_REGISTER) --config=config);
write_template!(html_path->"auth/connection.html"(crate::assets::AUTH_CONNECTION) --config=config);
write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config);
write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config);
2025-03-31 11:45:34 -04:00
write_template!(html_path->"profile/settings.html"(crate::assets::PROFILE_SETTINGS) --config=config);
2025-03-31 22:35:11 -04:00
write_template!(html_path->"profile/following.html"(crate::assets::PROFILE_FOLLOWING) --config=config);
write_template!(html_path->"profile/followers.html"(crate::assets::PROFILE_FOLLOWERS) --config=config);
write_template!(html_path->"profile/warning.html"(crate::assets::PROFILE_WARNING) --config=config);
write_template!(html_path->"profile/private.html"(crate::assets::PROFILE_PRIVATE) --config=config);
write_template!(html_path->"profile/blocked.html"(crate::assets::PROFILE_BLOCKED) --config=config);
write_template!(html_path->"profile/banned.html"(crate::assets::PROFILE_BANNED) --config=config);
write_template!(html_path->"profile/replies.html"(crate::assets::PROFILE_REPLIES) --config=config);
write_template!(html_path->"profile/media.html"(crate::assets::PROFILE_MEDIA) --config=config);
write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config);
write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config);
write_template!(html_path->"communities/feed.html"(crate::assets::COMMUNITIES_FEED) --config=config);
2025-03-31 11:45:34 -04:00
write_template!(html_path->"communities/settings.html"(crate::assets::COMMUNITIES_SETTINGS) --config=config);
2025-04-03 20:05:21 -04:00
write_template!(html_path->"communities/members.html"(crate::assets::COMMUNITIES_MEMBERS) --config=config);
2025-04-10 21:37:33 -04:00
write_template!(html_path->"communities/search.html"(crate::assets::COMMUNITIES_SEARCH) --config=config);
write_template!(html_path->"communities/create_post.html"(crate::assets::COMMUNITIES_CREATE_POST) --config=config);
2025-04-12 22:25:54 -04:00
write_template!(html_path->"communities/question.html"(crate::assets::COMMUNITIES_QUESTION) --config=config);
write_template!(html_path->"communities/questions.html"(crate::assets::COMMUNITIES_QUESTIONS) --config=config);
write_template!(html_path->"post/post.html"(crate::assets::POST_POST) -d "post" --config=config);
write_template!(html_path->"post/reposts.html"(crate::assets::POST_REPOSTS) --config=config);
write_template!(html_path->"post/quotes.html"(crate::assets::POST_QUOTES) --config=config);
write_template!(html_path->"post/likes.html"(crate::assets::POST_LIKES) --config=config);
2025-03-31 19:31:36 -04:00
write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config);
write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config);
write_template!(html_path->"timelines/following.html"(crate::assets::TIMELINES_FOLLOWING) --config=config);
write_template!(html_path->"timelines/all.html"(crate::assets::TIMELINES_ALL) --config=config);
2025-04-13 12:15:14 -04:00
write_template!(html_path->"timelines/home_questions.html"(crate::assets::TIMELINES_HOME_QUESTIONS) --config=config);
2025-04-13 12:58:44 -04:00
write_template!(html_path->"timelines/popular_questions.html"(crate::assets::TIMELINES_POPULAR_QUESTIONS) --config=config);
2025-04-13 12:15:14 -04:00
write_template!(html_path->"timelines/following_questions.html"(crate::assets::TIMELINES_FOLLOWING_QUESTIONS) --config=config);
write_template!(html_path->"timelines/all_questions.html"(crate::assets::TIMELINES_ALL_QUESTIONS) --config=config);
2025-05-18 16:43:56 -04:00
write_template!(html_path->"timelines/search.html"(crate::assets::TIMELINES_SEARCH) --config=config);
2025-03-31 19:31:36 -04:00
write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config);
write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config);
write_template!(html_path->"mod/file_report.html"(crate::assets::MOD_FILE_REPORT) --config=config);
write_template!(html_path->"mod/ip_bans.html"(crate::assets::MOD_IP_BANS) --config=config);
2025-04-06 13:43:12 -04:00
write_template!(html_path->"mod/profile.html"(crate::assets::MOD_PROFILE) --config=config);
2025-04-11 22:12:43 -04:00
write_template!(html_path->"mod/warnings.html"(crate::assets::MOD_WARNINGS) --config=config);
2025-05-02 20:51:19 -04:00
write_template!(html_path->"mod/stats.html"(crate::assets::MOD_STATS) --config=config);
2025-04-27 23:11:37 -04:00
write_template!(html_path->"chats/app.html"(crate::assets::CHATS_APP) -d "chats" --config=config);
write_template!(html_path->"chats/stream.html"(crate::assets::CHATS_STREAM) --config=config);
write_template!(html_path->"chats/message.html"(crate::assets::CHATS_MESSAGE) --config=config);
write_template!(html_path->"chats/channels.html"(crate::assets::CHATS_CHANNELS) --config=config);
2025-04-27 23:11:37 -04:00
2025-05-08 22:18:04 -04:00
write_template!(html_path->"stacks/list.html"(crate::assets::STACKS_LIST) -d "stacks" --config=config);
write_template!(html_path->"stacks/posts.html"(crate::assets::STACKS_POSTS) --config=config);
write_template!(html_path->"stacks/manage.html"(crate::assets::STACKS_MANAGE) --config=config);
html_path
}
/// Set up extra directories.
pub(crate) async fn init_dirs(config: &Config) {
2025-04-03 15:07:57 -04:00
create_dir_if_not_exists!(&config.dirs.templates);
2025-04-05 17:28:51 -04:00
create_dir_if_not_exists!(&config.dirs.docs);
2025-04-03 15:07:57 -04:00
// images
create_dir_if_not_exists!(&config.dirs.media);
let images_path = PathBufD::current().extend(&[config.dirs.media.as_str(), "images"]);
create_dir_if_not_exists!(&images_path);
create_dir_if_not_exists!(
&PathBufD::current().extend(&[config.dirs.media.as_str(), "avatars"])
);
create_dir_if_not_exists!(
&PathBufD::current().extend(&[config.dirs.media.as_str(), "community_avatars"])
);
create_dir_if_not_exists!(
&PathBufD::current().extend(&[config.dirs.media.as_str(), "banners"])
);
create_dir_if_not_exists!(
&PathBufD::current().extend(&[config.dirs.media.as_str(), "community_banners"])
);
2025-05-05 19:38:01 -04:00
create_dir_if_not_exists!(
&PathBufD::current().extend(&[config.dirs.media.as_str(), "uploads"])
);
write_if_track!(images_path->"default-avatar.svg"(DEFAULT_AVATAR) --config=config);
write_if_track!(images_path->"default-banner.svg"(DEFAULT_BANNER) --config=config);
write_if_track!(images_path->"favicon.svg"(FAVICON) --config=config);
// icons
create_dir_if_not_exists!(&PathBufD::current().join(config.dirs.icons.as_str()));
// langs
let langs_path = PathBufD::current().join("langs");
create_dir_if_not_exists!(&langs_path);
write_template!(langs_path->"en-US.toml"(LANG_EN_US));
}
2025-03-23 16:37:43 -04:00
/// A random ASCII value inserted into the URL of static assets to "break" the cache. Essentially just for cache busting.
pub(crate) static CACHE_BREAKER: LazyLock<String> = LazyLock::new(salt);
2025-03-23 16:37:43 -04:00
/// Create the initial template context.
pub(crate) async fn initial_context(
config: &Config,
lang: &LangFile,
user: &Option<User>,
) -> Context {
let mut ctx = Context::new();
ctx.insert("config", &config);
ctx.insert("pubsub", &PUBSUB_ENABLED);
ctx.insert("user", &user);
ctx.insert("use_user_theme", &true);
if let Some(ua) = user {
ctx.insert("is_helper", &ua.permissions.check_helper());
ctx.insert("is_manager", &ua.permissions.check_manager());
2025-05-05 19:38:01 -04:00
ctx.insert(
"is_supporter",
&ua.permissions.check(FinePermission::SUPPORTER),
);
2025-05-09 16:23:40 -04:00
ctx.insert("home", &ua.settings.default_timeline.relative_url());
} else {
ctx.insert("is_helper", &false);
ctx.insert("is_manager", &false);
2025-05-09 16:23:40 -04:00
ctx.insert("home", &DefaultTimelineChoice::default().relative_url());
}
2025-03-23 16:37:43 -04:00
ctx.insert("lang", &lang.data);
ctx.insert("random_cache_breaker", &CACHE_BREAKER.clone());
ctx
}