diff --git a/Cargo.lock b/Cargo.lock index ccf0b51..dc19faf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "tetratto" -version = "3.1.0" +version = "4.0.0" dependencies = [ "ammonia", "async-stripe", @@ -3313,7 +3313,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "3.1.0" +version = "4.0.0" dependencies = [ "async-recursion", "base16ct", @@ -3337,7 +3337,7 @@ dependencies = [ [[package]] name = "tetratto-l10n" -version = "3.1.0" +version = "4.0.0" dependencies = [ "pathbufd", "serde", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "3.1.0" +version = "4.0.0" dependencies = [ "ammonia", "chrono", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 408b1b0..305e1d9 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "3.1.0" +version = "4.0.0" edition = "2024" [features] diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 1fbf106..63f229d 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -87,6 +87,7 @@ 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"); +pub const TIMELINES_SEARCH: &str = include_str!("./public/html/timelines/search.html"); 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"); @@ -283,6 +284,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"timelines/popular_questions.html"(crate::assets::TIMELINES_POPULAR_QUESTIONS) --config=config); 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); + write_template!(html_path->"timelines/search.html"(crate::assets::TIMELINES_SEARCH) --config=config); 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); diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 90d5004..d0b27e1 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -15,6 +15,7 @@ version = "1.0.0" "general:link.reports" = "Reports" "general:link.ip_bans" = "IP bans" "general:link.stats" = "Stats" +"general:link.search" = "Search" "general:action.save" = "Save" "general:action.delete" = "Delete" "general:action.purge" = "Purge" @@ -109,7 +110,6 @@ version = "1.0.0" "communities:label.repost" = "Repost" "communities:label.quote_post" = "Quote post" "communities:label.expand_original" = "Expand original" -"communities:label.search" = "Search" "communities:label.search_results" = "Search results" "communities:label.query" = "Query" "communities:label.join_new" = "Join new" diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs index edfb8de..2d92375 100644 --- a/crates/app/src/macros.rs +++ b/crates/app/src/macros.rs @@ -97,7 +97,7 @@ macro_rules! get_lang { #[macro_export] macro_rules! check_user_blocked_or_private { - ($user:ident, $other_user:ident, $data:ident, $jar:ident) => { + ($user:expr, $other_user:ident, $data:ident, $jar:ident) => { // check require_account if $user.is_none() && $other_user.settings.require_account { return Err(Html( diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index c4d67f6..b407bfe 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -755,6 +755,38 @@ select:focus { border-bottom-right-radius: var(--radius); } +.pillmenu:not(.rows) .row { + display: contents; +} + +.pillmenu.rows { + flex-direction: column; +} + +.pillmenu.rows .row { + display: flex; +} + +.pillmenu.rows a { + border-radius: 0; +} + +.pillmenu.rows .row:first-of-type a:first-child { + border-top-left-radius: var(--radius); +} + +.pillmenu.rows .row:first-of-type a:last-child { + border-top-right-radius: var(--radius); +} + +.pillmenu.rows .row:last-of-type a:first-child { + border-bottom-left-radius: var(--radius); +} + +.pillmenu.rows .row:last-of-type a:last-child { + border-bottom-right-radius: var(--radius); +} + @media screen and (max-width: 900px) { .pillmenu { /* convert into a sidemenu */ @@ -762,9 +794,9 @@ select:focus { } .pillmenu a:first-child { + border-bottom-left-radius: 0; border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); - border-bottom-left-radius: 0; } .pillmenu a:last-child { @@ -772,6 +804,17 @@ select:focus { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); } + + .pillmenu.rows .row { + display: contents; + } + + .pillmenu.rows .row:first-of-type a:first-child, + .pillmenu.rows .row:first-of-type a:last-child, + .pillmenu.rows .row:last-of-type a:first-child, + .pillmenu.rows .row:last-of-type a:last-child { + border-radius: 0; + } } /* notification */ diff --git a/crates/app/src/public/html/auth/connection.html b/crates/app/src/public/html/auth/connection.html index b796522..fafa8c9 100644 --- a/crates/app/src/public/html/auth/connection.html +++ b/crates/app/src/public/html/auth/connection.html @@ -72,4 +72,4 @@ config.connections.last_fm_key %} }, 500); }, 1000); -{% endif %} {% endblock %} +{%- endif %} {% endblock %} diff --git a/crates/app/src/public/html/chats/app.html b/crates/app/src/public/html/chats/app.html index 59db607..5f96774 100644 --- a/crates/app/src/public/html/chats/app.html +++ b/crates/app/src/public/html/chats/app.html @@ -7,15 +7,15 @@ hide_user_menu=true) }} class="flex gap-2 items-center active" onclick="toggle_sidebars(event)" > - {{ icon "panel-left" }} {% if community %} + {{ icon "panel-left" }} {% if community -%} - {% if community.context.display_name %} {{ + {% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} - {% endif %} + {%- endif %} {% else %} {{ text "chats:label.my_chats" }} - {% endif %} + {%- endif %} @@ -27,36 +27,36 @@ hide_user_menu=true) }} > {{ icon "message-circle" }} - {% for community in communities %} {% if community.id != 0 %} + {% for community in communities %} {% if community.id != 0 -%} {{ components::community_avatar(id=community.id, community=community, size="48px") }} - {% endif %} {% endfor %} + {%- endif %} {% endfor %} -{% endif %} {%- endmacro %} {% macro last_fm_playing(state, size="60px") -%} {% +{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size="60px") -%} {% if state and state.data %}
@@ -967,7 +968,7 @@ if state and state.data %} > - {% if state.data.duration_ms and state.data.duration_ms != "0" %} + {% if state.data.duration_ms and state.data.duration_ms != "0" -%} - {% endif %} + {%- endif %}
-{% endif %} {%- endmacro %} {% macro connection_icon(key) -%} +{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%}
- {% if key == "Spotify" %} + {% if key == "Spotify" -%} {{ icon "spotify" }} {% elif key == "LastFm" %} {{ icon "last_fm" }} - {% endif %} + {%- endif %}
{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == "LastFm" %} https://last.fm/user/{{ -value[0].data.name }} {% endif %} {%- endmacro %} {% macro +value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%} @@ -1087,7 +1088,7 @@ grouped=false) -%} {{ text "auth:link.settings" }} - {% if is_helper %} + {% if is_helper -%} {{ text "general:label.mod" }} @@ -1109,7 +1110,7 @@ grouped=false) -%} {{ icon "chart-line" }} {{ text "general:link.stats" }} - {% endif %} + {%- endif %} {{ config.name }} @@ -1166,10 +1167,10 @@ other_user.connections.Spotify[1].data.track %} }} -{% endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, +{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}
{{ self::avatar(username=user.username, size="42px", @@ -1178,13 +1179,13 @@ show_kick=false, secondary=false) -%}
{{ self::full_username(user=user) }}
{{ self::user_status(other_user=user) }}
- {% if show_menu %} + {% if show_menu -%}
- {% endif %} + {%- endif %} {%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%} - {% endif %} {% if not user.settings.private_chats or + {%- endif %} {% if not user.settings.private_chats or is_following_you %} - {% endif %} {% if is_helper %} + {%- endif %} {% if is_helper -%} {{ text "general:action.manage" }} - {% endif %} + {%- endif %} - {% endif %} {% if not profile.settings.private_communities or + {%- endif %} {% if not profile.settings.private_communities or is_self or is_helper %}
@@ -337,7 +337,7 @@ {% endfor %}
- {% endif %} + {%- endif %}
{% for key, value in profile.connections %} {% if @@ -355,7 +355,7 @@ {{ icon "external-link" }} - {% endif %} {% endfor %} + {%- endif %} {% endfor %}
@@ -365,7 +365,7 @@ -{% if not is_self and profile.settings.warning %} +{% if not is_self and profile.settings.warning -%} -{% endif %} {% if not use_user_theme %} {{ components::theme(user=profile, -theme_preference=profile.settings.profile_theme) }} {% endif %} {% endblock %} +{%- endif %} {% if not use_user_theme -%} {{ components::theme(user=profile, +theme_preference=profile.settings.profile_theme) }} {%- endif %} {% endblock %} diff --git a/crates/app/src/public/html/profile/blocked.html b/crates/app/src/public/html/profile/blocked.html index ebae7c7..15014d1 100644 --- a/crates/app/src/public/html/profile/blocked.html +++ b/crates/app/src/public/html/profile/blocked.html @@ -18,7 +18,7 @@ {{ text "auth:label.blocked_profile_message" }}
- {% if user %} {% if not is_blocking %} + {% if user -%} {% if not is_blocking -%} - {% endif %} + {%- endif %} - {% endif %} + {%- endif %} {{ icon "x" }} diff --git a/crates/app/src/public/html/profile/posts.html b/crates/app/src/public/html/profile/posts.html index e4a9ee3..6577a31 100644 --- a/crates/app/src/public/html/profile/posts.html +++ b/crates/app/src/public/html/profile/posts.html @@ -5,7 +5,7 @@ profile.settings.allow_anonymous_questions) %} {{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header) }}
-{% endif %} {% if not tag and pinned|length != 0 %} +{%- endif %} {% if not tag and pinned|length != 0 -%}
{{ icon "pin" }} @@ -15,37 +15,51 @@ profile.settings.allow_anonymous_questions) %}
{% for post in pinned %} - {% if post[2].read_access == "Everybody" %} - {% if post[0].context.repost and post[0].context.repost.reposting %} + {% if post[2].read_access == "Everybody" -%} + {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], can_manage_post=is_self) }} - {% endif %} - {% endif %} + {%- endif %} + {%- endif %} {% endfor %}
-{% endif %} +{%- endif %}
-
- {% if not tag %} {{ icon "clock" }} - {{ text "auth:label.recent_posts" }} - {% else %} {{ icon "tag" }} - {{ text "auth:label.recent_with_tag" }}: {{ tag }} - {% endif %} +
{% for post in posts %} - {% if post[2].read_access == "Everybody" %} - {% if post[0].context.repost and post[0].context.repost.reposting %} + {% if post[2].read_access == "Everybody" -%} + {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], can_manage_post=is_self) }} - {% endif %} - {% endif %} + {%- endif %} + {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length, key="&tag=", value=tag) }} diff --git a/crates/app/src/public/html/profile/private.html b/crates/app/src/public/html/profile/private.html index 65e2850..c89e2a8 100644 --- a/crates/app/src/public/html/profile/private.html +++ b/crates/app/src/public/html/profile/private.html @@ -18,10 +18,10 @@ {{ text "auth:label.private_profile_message" }}
- {% if user %} {% if not is_following %} + {% if user -%} {% if not is_following -%} - {% endif %} {% if not is_blocking %} + {%- endif %} {% if not is_blocking -%} - {% endif %} + {%- endif %} - {% endif %} + {%- endif %} {{ icon "x" }} diff --git a/crates/app/src/public/html/profile/settings.html b/crates/app/src/public/html/profile/settings.html index a86fe69..54f010c 100644 --- a/crates/app/src/public/html/profile/settings.html +++ b/crates/app/src/public/html/profile/settings.html @@ -2,12 +2,12 @@ Settings - {{ config.name }} {% endblock %} {% block body %} {{ macros::nav() }}
- {% if profile.id != user.id %} + {% if profile.id != user.id -%}
{{ icon "skull" }} Editing other user's settings! Please be careful.
- {% endif %} + {%- endif %}
@@ -65,12 +65,12 @@ {{ text "settings:tab.uploads" }} - {% if config.stripe %} + {% if config.stripe -%} {{ icon "credit-card" }} {{ text "settings:tab.billing" }} - {% endif %} + {%- endif %}
@@ -84,59 +84,59 @@ > {% for stack in stacks %} @@ -265,7 +265,7 @@
- {% if profile.totp|length == 0 %} + {% if profile.totp|length == 0 -%} - {% endif %} + {%- endif %}
@@ -569,7 +569,7 @@
- {% if config.stripe %} + {% if config.stripe -%}
{{ icon "star" }} @@ -577,7 +577,7 @@
- {% if is_supporter %} + {% if is_supporter -%}

You are a supporter! Thank you for all that you do. You can manage your billing @@ -617,6 +617,7 @@

  • Create infinite stack timelines
  • Ability to upload images to posts
  • Save infinite post drafts
  • +
  • Ability to search through all posts
  • - {% endif %} + {%- endif %}
    - {% endif %} + {%- endif %}
    @@ -727,7 +728,7 @@ " >{{ token[1] }} - {% if is_helper %} + {% if is_helper -%} {% else %} {{ token[0] }} - {% endif %} + {%- endif %} {{ token[2] }}
    @@ -754,7 +755,7 @@ - {% if user %} + {% if user -%}
    - {% endif %} {% if user and use_user_theme %} {{ + {%- endif %} {% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }} - {% endif %} {% if user and user.connections.Spotify and + {%- endif %} {% if user and user.connections.Spotify and config.connections.spotify_client_id and user.connections.Spotify[0].data.token and user.connections.Spotify[0].data.refresh_token %} @@ -439,6 +439,6 @@ macros -%} } }, 150); - {% endif %} + {%- endif %} diff --git a/crates/app/src/public/html/stacks/list.html b/crates/app/src/public/html/stacks/list.html index c2bd30e..4e35b72 100644 --- a/crates/app/src/public/html/stacks/list.html +++ b/crates/app/src/public/html/stacks/list.html @@ -2,7 +2,7 @@ My stacks - {{ config.name }} {% endblock %} {% block body %} {{ macros::nav() }}
    - {{ macros::timelines_nav(selected="stacks") }} {% if user %} + {{ macros::timelines_nav(selected="stacks") }} {% if user -%}
    {{ text "stacks:label.create_new" }} @@ -30,7 +30,7 @@
    - {% endif %} + {%- endif %}
    diff --git a/crates/app/src/public/html/stacks/manage.html b/crates/app/src/public/html/stacks/manage.html index bbe9745..7f7cf47 100644 --- a/crates/app/src/public/html/stacks/manage.html +++ b/crates/app/src/public/html/stacks/manage.html @@ -25,13 +25,13 @@ @@ -71,13 +71,13 @@ + + {% if profile -%} + + {%- endif %} + + +
    + + {% if config.manuals.search_help -%} + + Search help + + {%- endif %} + + {%- endif %} + + + {% for post in list %} + {% if post[2].read_access == "Everybody" -%} + {% if post[0].context.repost and post[0].context.repost.reposting -%} + {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} + {% else %} + {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} + {%- endif %} + {%- endif %} + {% endfor %} + + {% if profile -%} + {{ components::pagination(page=page, items=list|length, key="&profile=" ~ profile.id, value="&query=" ~ query) }} + {% else %} + {{ components::pagination(page=page, items=list|length, key="&query=" ~ query) }} + {%- endif %} +
    +
    +
    +{% endblock %} diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 1edc42b..d540e02 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -1,12 +1,15 @@ use super::{PaginatedQuery, render_error}; -use crate::{State, assets::initial_context, get_lang, get_user_from_token}; +use crate::{ + assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, State, +}; use axum::{ extract::{Path, Query}, response::{Html, IntoResponse}, Extension, }; use axum_extra::extract::CookieJar; -use tetratto_core::model::{requests::ActionType, Error}; +use serde::Deserialize; +use tetratto_core::model::{permissions::FinePermission, requests::ActionType, Error}; use std::fs::read_to_string; use pathbufd::PathBufD; @@ -497,3 +500,96 @@ pub async fn markdown_document_request( // return Ok(Html(data.1.render("misc/markdown.html", &context).unwrap())) } + +#[derive(Deserialize)] +pub struct SearchQuery { + #[serde(default)] + pub query: String, + #[serde(default)] + pub profile: usize, + #[serde(default)] + pub page: usize, +} + +/// `/search` +pub async fn search_request( + jar: CookieJar, + Extension(data): Extension, + Query(mut req): Query, +) -> impl IntoResponse { + let data = data.read().await; + let user = match get_user_from_token!(jar, data.0) { + Some(ua) => ua, + None => { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + }; + + if req.profile == 0 && !user.permissions.check(FinePermission::SUPPORTER) { + req.query = String::new(); + } + + req.query = req.query.trim().replace(" ", " & "); // change spaces into & for tsquery + + let ignore_users = data.0.get_userblocks_receivers(user.id).await; + + let list = if req.query.is_empty() { + Vec::new() + } else { + if req.profile != 0 { + match data + .0 + .get_posts_by_user_searched(req.profile, 12, req.page, &req.query, &Some(&user)) + .await + { + Ok(l) => match data + .0 + .fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => l, + Err(_) => Vec::new(), + }, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + } + } else { + match data.0.get_posts_searched(12, req.page, &req.query).await { + Ok(l) => match data + .0 + .fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => l, + Err(_) => Vec::new(), + }, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + } + } + }; + + let profile = if req.profile != 0 { + Some(match data.0.get_user_by_id(req.profile).await { + Ok(ua) => { + check_user_blocked_or_private!(Some(user.clone()), ua, data, jar); + ua + } + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }) + } else { + None + }; + + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0, lang, &Some(user)).await; + + context.insert("list", &list); + context.insert("profile", &profile); + context.insert("query", &req.query); + context.insert("page", &req.page); + + Ok(Html( + data.1.render("timelines/search.html", &context).unwrap(), + )) +} diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 0e8b501..9922cd1 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -28,6 +28,7 @@ pub fn routes() -> Router { .route("/popular", get(misc::popular_request)) .route("/following", get(misc::following_request)) .route("/all", get(misc::all_request)) + .route("/search", get(misc::search_request)) // question timelines .route("/questions", get(misc::index_questions_request)) .route("/popular/questions", get(misc::popular_questions_request)) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index adc8e8d..9af2d84 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-core" -version = "3.1.0" +version = "4.0.0" edition = "2024" [features] diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 4469bce..d1789e7 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -196,6 +196,21 @@ pub struct StripeConfig { pub billing_portal_url: String, } +/// Manuals config (search help, etc) +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ManualsConfig { + /// The page shown for help with search syntax. + pub search_help: String, +} + +impl Default for ManualsConfig { + fn default() -> Self { + Self { + search_help: "".to_string(), + } + } +} + /// Configuration file #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Config { @@ -259,6 +274,9 @@ pub struct Config { pub html_footer_path: String, #[serde(default)] pub stripe: Option, + /// The relative paths to manuals. + #[serde(default)] + pub manuals: ManualsConfig, } fn default_name() -> String { @@ -307,12 +325,15 @@ fn default_banned_usernames() -> Vec { "moderator".to_string(), "api".to_string(), "communities".to_string(), + "community".to_string(), "notifs".to_string(), "notification".to_string(), "post".to_string(), "void".to_string(), "anonymous".to_string(), + "stacks".to_string(), "stack".to_string(), + "search".to_string(), ] } @@ -328,6 +349,10 @@ fn default_connections() -> ConnectionsConfig { ConnectionsConfig::default() } +fn default_manuals() -> ManualsConfig { + ManualsConfig::default() +} + impl Default for Config { fn default() -> Self { Self { @@ -348,6 +373,7 @@ impl Default for Config { connections: default_connections(), html_footer_path: String::new(), stripe: None, + manuals: default_manuals(), } } } diff --git a/crates/core/src/database/drivers/sql/create_posts.sql b/crates/core/src/database/drivers/sql/create_posts.sql index 76350d7..640dbfa 100644 --- a/crates/core/src/database/drivers/sql/create_posts.sql +++ b/crates/core/src/database/drivers/sql/create_posts.sql @@ -13,5 +13,6 @@ CREATE TABLE IF NOT EXISTS posts ( comment_count INT NOT NULL, -- ... uploads TEXT NOT NULL, - is_deleted INT NOT NULL + is_deleted INT NOT NULL, + tsvector_content tsvector GENERATED ALWAYS AS (to_tsvector ('english', coalesce(content, ''))) STORED ) diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index d5c1466..abfda0c 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -52,6 +52,37 @@ macro_rules! private_post_replying { continue; } }; + + ($post:ident, $replying_posts:ident, id=$user_id:ident, $data:ident) => { + // post owner is not following us + // check if we're the owner of the post the post is replying to + // all routes but 1 must lead to continue + if let Some(replying) = $post.replying_to { + if replying != 0 { + if let Some(post) = $replying_posts.get(&replying) { + // we've seen this post before + if post.owner != $user_id { + // we aren't the owner of this post, + // so we can't see their comment + continue; + } + } else { + // we haven't seen this post before + let post = $data.get_post_by_id(replying).await?; + + if post.owner != $user_id { + continue; + } + + $replying_posts.insert(post.id, post); + } + } else { + continue; + } + } else { + continue; + } + }; } impl DataManager { @@ -317,6 +348,7 @@ impl DataManager { let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); + let mut replying_posts: HashMap = HashMap::new(); for post in posts { if post.is_deleted { @@ -355,9 +387,8 @@ impl DataManager { if user_id != ua.id { if let Some(is_following) = seen_user_follow_statuses.get(&(ua.id, user_id)) { - if !is_following && (ua.id != user_id) { - // post owner is not following us - continue; + if !is_following { + private_post_replying!(post, replying_posts, id = user_id, self); } } else { if self @@ -367,7 +398,7 @@ impl DataManager { { // post owner is not following us seen_user_follow_statuses.insert((ua.id, user_id), false); - continue; + private_post_replying!(post, replying_posts, id = user_id, self); } seen_user_follow_statuses.insert((ua.id, user_id), true); @@ -428,9 +459,9 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT context LIKE '%\"is_profile_pinned\":true%' {} ORDER BY created DESC LIMIT $2 OFFSET $3", + "SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $2 OFFSET $3", if hide_nsfw { - "AND NOT context LIKE '%\"is_nsfw\":true%'" + "AND NOT (context::json->>'is_nsfw')::boolean" } else { "" } @@ -446,6 +477,101 @@ impl DataManager { Ok(res.unwrap()) } + /// Get all posts from the given user (searched). + /// + /// # Arguments + /// * `id` - the ID of the user the requested posts belong to + /// * `batch` - the limit of posts in each page + /// * `page` - the page number + /// * `text_query` - the search query + /// * `user` - the user who is viewing the posts + pub async fn get_posts_by_user_searched( + &self, + id: usize, + batch: usize, + page: usize, + text_query: &str, + user: &Option<&User>, + ) -> Result> { + let other_user = self.get_user_by_id(id).await?; + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + // check if we should hide nsfw posts + let mut hide_nsfw: bool = true; + + if let Some(ua) = user { + if ua.id == other_user.id { + hide_nsfw = false + } + } + + if other_user.settings.private_profile { + hide_nsfw = false; + } + + // ... + let res = query_rows!( + &conn, + &format!( + "SELECT * FROM posts WHERE owner = $1 AND tsvector_content @@ to_tsquery($2) AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $3 OFFSET $4", + if hide_nsfw { + "AND NOT (context::json->>'is_nsfw')::boolean" + } else { + "" + } + ), + params![ + &(id as i64), + &text_query, + &(batch as i64), + &((page * batch) as i64) + ], + |x| { Self::get_post_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("post".to_string())); + } + + Ok(res.unwrap()) + } + + /// Get all post (searched). + /// + /// # Arguments + /// * `batch` - the limit of posts in each page + /// * `page` - the page number + /// * `text_query` - the search query + pub async fn get_posts_searched( + &self, + batch: usize, + page: usize, + text_query: &str, + ) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + // ... + let res = query_rows!( + &conn, + "SELECT * FROM posts WHERE tsvector_content @@ to_tsquery($1) AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean ORDER BY created DESC LIMIT $2 OFFSET $3", + params![&text_query, &(batch as i64), &((page * batch) as i64)], + |x| { Self::get_post_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("post".to_string())); + } + + Ok(res.unwrap()) + } + /// Get all posts from the given user with the given tag (from most recent). /// /// # Arguments @@ -487,7 +613,7 @@ impl DataManager { &format!( "SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 {} ORDER BY created DESC LIMIT $3 OFFSET $4", if hide_nsfw { - "AND NOT context LIKE '%\"is_nsfw\":true%'" + "AND NOT (context::json->>'is_nsfw')::boolean" } else { "" } diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index ff1a8f0..16b655c 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-l10n" -version = "3.1.0" +version = "4.0.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 1b64125..18f0b28 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-shared" -version = "3.1.0" +version = "4.0.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/example/public/footer.html b/example/public/footer.html index 96fe51d..803b5b3 100644 --- a/example/public/footer.html +++ b/example/public/footer.html @@ -1,5 +1,5 @@ -{% if user %} +{% if user -%} -{% endif %} +{%- endif %} diff --git a/sql_changes/posts_tscvector_content.sql b/sql_changes/posts_tscvector_content.sql new file mode 100644 index 0000000..3d56f8c --- /dev/null +++ b/sql_changes/posts_tscvector_content.sql @@ -0,0 +1,4 @@ +ALTER TABLE posts +ADD COLUMN tsvector_content tsvector GENERATED ALWAYS AS (to_tsvector ('english', coalesce(content, ''))) STORED; + +CREATE INDEX tsvector_content_idx ON posts USING GIN (tsvector_content);