RSS for posts
This commit is contained in:
parent
bb118f6144
commit
8fac396d7e
13 changed files with 233 additions and 14 deletions
83
Cargo.lock
generated
83
Cargo.lock
generated
|
|
@ -370,6 +370,19 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atom_syndication"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"diligent-date-parser",
|
||||
"never",
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
|
@ -953,6 +966,7 @@ dependencies = [
|
|||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
|
|
@ -988,6 +1002,37 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
|
@ -1000,6 +1045,15 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diligent-date-parser"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
|
|
@ -2062,6 +2116,12 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "never"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
|
@ -2518,6 +2578,16 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
|
|
@ -2800,6 +2870,18 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rss"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf"
|
||||
dependencies = [
|
||||
"atom_syndication",
|
||||
"derive_builder",
|
||||
"never",
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.7.0"
|
||||
|
|
@ -2969,6 +3051,7 @@ dependencies = [
|
|||
"password-auth",
|
||||
"pulldown-cmark",
|
||||
"rand 0.9.0",
|
||||
"rss",
|
||||
"rust-embed",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ mime_guess = "2.0.5"
|
|||
password-auth = "1.0.0"
|
||||
pulldown-cmark = "0.13.0"
|
||||
rand = "0.9.0"
|
||||
rss = "2.0.12"
|
||||
rust-embed = { version = "8.7.0", features = ["axum", "debug-embed"] }
|
||||
sea-orm = { version = "1.1.8", features = [
|
||||
"sqlx-sqlite",
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ Still very much an early WIP.
|
|||
### Roadmap
|
||||
|
||||
- [ ] Favicon from post
|
||||
- [ ] Bulk edit tag
|
||||
- [ ] RSS feed
|
||||
- [ ] Logging
|
||||
- [ ] Improved error handling
|
||||
- [ ] Bulk edit tag
|
||||
- [ ] Caching
|
||||
- [ ] Lossless compression
|
||||
- [ ] User management
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ use crate::{
|
|||
};
|
||||
|
||||
pub(crate) const APPLICATION_NAME_KEY: &str = "APPLICATION_NAME";
|
||||
pub(crate) const BASE_URL_KEY: &str = "BASE_URL";
|
||||
pub(crate) const AGE_CONFIRMATION_KEY: &str = "AGE_CONFIRMATION";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppConfig {
|
||||
pub(crate) application_name: String,
|
||||
pub(crate) base_url: String,
|
||||
pub(crate) age_confirmation: bool,
|
||||
}
|
||||
|
||||
|
|
@ -24,6 +26,14 @@ impl AppConfig {
|
|||
Some(row) => row.data.as_str().unwrap_or("Samey").to_owned(),
|
||||
None => "Samey".to_owned(),
|
||||
};
|
||||
let base_url = match SameyConfig::find()
|
||||
.filter(samey_config::Column::Key.eq(BASE_URL_KEY))
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
Some(row) => row.data.as_str().unwrap_or("").to_owned(),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
let age_confirmation = match SameyConfig::find()
|
||||
.filter(samey_config::Column::Key.eq(AGE_CONFIRMATION_KEY))
|
||||
.one(db)
|
||||
|
|
@ -34,6 +44,7 @@ impl AppConfig {
|
|||
};
|
||||
Ok(Self {
|
||||
application_name,
|
||||
base_url,
|
||||
age_confirmation,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,9 @@ pub async fn get_router(
|
|||
fs::create_dir_all(files_dir.as_ref()).await?;
|
||||
|
||||
let session_store = SessionStorage::new(db.clone());
|
||||
let session_layer = SessionManagerLayer::new(session_store);
|
||||
let session_layer = SessionManagerLayer::new(session_store).with_expiry(
|
||||
tower_sessions::Expiry::OnInactivity(time::Duration::weeks(1)),
|
||||
);
|
||||
let auth_layer = AuthManagerLayerBuilder::new(Backend::new(db), session_layer).build();
|
||||
|
||||
Ok(Router::new()
|
||||
|
|
@ -134,6 +136,7 @@ pub async fn get_router(
|
|||
.route_with_tsr("/posts/{page}", get(posts_page))
|
||||
// Other routes
|
||||
.route_with_tsr("/remove", delete(remove_field))
|
||||
.route("/posts.xml", get(rss_page))
|
||||
.route("/", get(index))
|
||||
.with_state(state)
|
||||
.nest_service("/files", ServeDir::new(files_dir))
|
||||
|
|
|
|||
13
src/query.rs
13
src/query.rs
|
|
@ -1,5 +1,6 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use migration::{Expr, Query};
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoSimpleExpr,
|
||||
|
|
@ -20,6 +21,10 @@ use crate::{
|
|||
pub(crate) struct PostOverview {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) thumbnail: String,
|
||||
pub(crate) media: String,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) uploaded_at: NaiveDateTime,
|
||||
pub(crate) tags: Option<String>,
|
||||
pub(crate) media_type: String,
|
||||
pub(crate) rating: String,
|
||||
|
|
@ -54,6 +59,10 @@ pub(crate) fn search_posts(
|
|||
let mut query = SameyPost::find()
|
||||
.select_only()
|
||||
.column(samey_post::Column::Id)
|
||||
.column(samey_post::Column::Media)
|
||||
.column(samey_post::Column::Title)
|
||||
.column(samey_post::Column::Description)
|
||||
.column(samey_post::Column::UploadedAt)
|
||||
.column(samey_post::Column::Thumbnail)
|
||||
.column(samey_post::Column::Rating)
|
||||
.column(samey_post::Column::MediaType)
|
||||
|
|
@ -77,6 +86,10 @@ pub(crate) fn search_posts(
|
|||
let mut query = SameyPost::find()
|
||||
.select_only()
|
||||
.column(samey_post::Column::Id)
|
||||
.column(samey_post::Column::Media)
|
||||
.column(samey_post::Column::Title)
|
||||
.column(samey_post::Column::Description)
|
||||
.column(samey_post::Column::UploadedAt)
|
||||
.column(samey_post::Column::Thumbnail)
|
||||
.column(samey_post::Column::Rating)
|
||||
.column(samey_post::Column::MediaType)
|
||||
|
|
|
|||
85
src/views.rs
85
src/views.rs
|
|
@ -29,7 +29,7 @@ use tokio::{task::spawn_blocking, try_join};
|
|||
use crate::{
|
||||
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
||||
auth::{AuthSession, Credentials, User},
|
||||
config::{AGE_CONFIRMATION_KEY, APPLICATION_NAME_KEY},
|
||||
config::{AGE_CONFIRMATION_KEY, APPLICATION_NAME_KEY, BASE_URL_KEY},
|
||||
entities::{
|
||||
prelude::{
|
||||
SameyConfig, SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag,
|
||||
|
|
@ -90,6 +90,63 @@ pub(crate) async fn index(
|
|||
))
|
||||
}
|
||||
|
||||
// RSS view
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "fragments/rss_entry.html")]
|
||||
struct RssEntryTemplate<'a> {
|
||||
post: PostOverview,
|
||||
base_url: &'a str,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub(crate) async fn rss_page(
|
||||
State(AppState { app_config, db, .. }): State<AppState>,
|
||||
Query(query): Query<PostsQuery>,
|
||||
) -> Result<impl IntoResponse, SameyError> {
|
||||
let app_config = app_config.read().await;
|
||||
let application_name = app_config.application_name.clone();
|
||||
let base_url = app_config.base_url.clone();
|
||||
drop(app_config);
|
||||
|
||||
let tags = query
|
||||
.tags
|
||||
.as_ref()
|
||||
.map(|tags| tags.split_whitespace().collect::<Vec<_>>());
|
||||
|
||||
let posts = search_posts(tags.as_ref(), None)
|
||||
.paginate(&db, 20)
|
||||
.fetch_page(0)
|
||||
.await?;
|
||||
|
||||
let channel = rss::ChannelBuilder::default()
|
||||
.title(&application_name)
|
||||
.link(&base_url)
|
||||
.items(
|
||||
posts
|
||||
.into_iter()
|
||||
.map(|post| {
|
||||
rss::ItemBuilder::default()
|
||||
.title(post.tags.clone())
|
||||
.pub_date(post.uploaded_at.and_utc().to_rfc2822())
|
||||
.link(format!("{}/post/{}", &base_url, post.id))
|
||||
.content(
|
||||
RssEntryTemplate {
|
||||
post,
|
||||
base_url: &base_url,
|
||||
}
|
||||
.render()
|
||||
.ok(),
|
||||
)
|
||||
.build()
|
||||
})
|
||||
.collect_vec(),
|
||||
)
|
||||
.build();
|
||||
|
||||
Ok(channel.to_string())
|
||||
}
|
||||
|
||||
// Auth views
|
||||
|
||||
#[derive(Template)]
|
||||
|
|
@ -1097,6 +1154,7 @@ pub(crate) async fn sort_pool(
|
|||
#[template(path = "pages/settings.html")]
|
||||
struct SettingsTemplate {
|
||||
application_name: String,
|
||||
base_url: String,
|
||||
age_confirmation: bool,
|
||||
}
|
||||
|
||||
|
|
@ -1110,6 +1168,7 @@ pub(crate) async fn settings(
|
|||
|
||||
let app_config = app_config.read().await;
|
||||
let application_name = app_config.application_name.clone();
|
||||
let base_url = app_config.base_url.clone();
|
||||
let age_confirmation = app_config.age_confirmation;
|
||||
drop(app_config);
|
||||
|
||||
|
|
@ -1129,6 +1188,7 @@ pub(crate) async fn settings(
|
|||
Ok(Html(
|
||||
SettingsTemplate {
|
||||
application_name,
|
||||
base_url,
|
||||
age_confirmation,
|
||||
}
|
||||
.render_with_values(&values)?,
|
||||
|
|
@ -1138,6 +1198,7 @@ pub(crate) async fn settings(
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct UpdateSettingsForm {
|
||||
application_name: String,
|
||||
base_url: String,
|
||||
age_confirmation: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
@ -1164,6 +1225,16 @@ pub(crate) async fn update_settings(
|
|||
});
|
||||
}
|
||||
|
||||
let _ = mem::replace(
|
||||
&mut app_config.write().await.base_url,
|
||||
body.base_url.clone(),
|
||||
);
|
||||
configs.push(samey_config::ActiveModel {
|
||||
key: Set(BASE_URL_KEY.into()),
|
||||
data: Set(body.base_url.into()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let age_confirmation = body.age_confirmation.is_some();
|
||||
let _ = mem::replace(
|
||||
&mut app_config.write().await.age_confirmation,
|
||||
|
|
@ -1248,6 +1319,10 @@ pub(crate) async fn view_post_page(
|
|||
Some(parent_post) => Some(PostOverview {
|
||||
id: parent_id,
|
||||
thumbnail: parent_post.thumbnail,
|
||||
title: parent_post.title,
|
||||
description: parent_post.description,
|
||||
uploaded_at: parent_post.uploaded_at,
|
||||
media: parent_post.media,
|
||||
tags: Some(
|
||||
get_tags_for_post(post_id)
|
||||
.all(&db)
|
||||
|
|
@ -1277,6 +1352,10 @@ pub(crate) async fn view_post_page(
|
|||
children_posts.push(PostOverview {
|
||||
id: child_post.id,
|
||||
thumbnail: child_post.thumbnail,
|
||||
title: child_post.title,
|
||||
description: child_post.description,
|
||||
uploaded_at: child_post.uploaded_at,
|
||||
media: child_post.media,
|
||||
tags: Some(
|
||||
get_tags_for_post(child_post.id)
|
||||
.all(&db)
|
||||
|
|
@ -1410,6 +1489,10 @@ pub(crate) async fn submit_post_details(
|
|||
Some(parent_post) => Some(PostOverview {
|
||||
id: parent_id,
|
||||
thumbnail: parent_post.thumbnail,
|
||||
title: parent_post.title,
|
||||
description: parent_post.description,
|
||||
uploaded_at: parent_post.uploaded_at,
|
||||
media: parent_post.media,
|
||||
tags: Some(
|
||||
get_tags_for_post(post_id)
|
||||
.all(&db)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<video
|
||||
id="media"
|
||||
src="/files/{{ post.media }}"
|
||||
controls="controls"
|
||||
:style="{ width: '100%', height: '100%', 'max-width': width + 'px', 'max-height': height + 'px', 'aspect-ratio': width + ' / ' + height }"
|
||||
controls="true"
|
||||
style="width: 100%; height: 100%"
|
||||
:style="{ 'max-width': width + 'px', 'max-height': height + 'px', 'aspect-ratio': width + ' / ' + height }"
|
||||
></video>
|
||||
|
|
|
|||
11
templates/fragments/rss_entry.html
Normal file
11
templates/fragments/rss_entry.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<h1>
|
||||
{% if let Some(title) = post.title %}{{ title }}{% else %}Details{% endif %}
|
||||
</h1>
|
||||
{% match post.media_type.as_ref() %}{% when "image" %}
|
||||
<img src="{{ base_url }}/files/{{ post.media }}" />
|
||||
{% when "video" %}
|
||||
<video src="{{ base_url }}/files/{{ post.media }}" controls="true"></video>
|
||||
{% else %}{% endmatch %}{% if let Some(description) = post.description %}
|
||||
<h2>Description</h2>
|
||||
<div>{{ description | markdown }}</div>
|
||||
{% endif %}
|
||||
|
|
@ -18,10 +18,17 @@ parent_post %}
|
|||
{% else %}
|
||||
<article id="parent-post" hx-swap-oob="outerHTML" hidden></article>
|
||||
{% endif %}
|
||||
<ul id="tags-list" hx-swap-oob="outerHTML">
|
||||
<article id="tags-list" hx-swap-oob="outerHTML">
|
||||
<h2>Tags</h2>
|
||||
{% if tags.is_empty() %}
|
||||
<p>No tags in post. Consider adding some!</p>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for tag in tags %}
|
||||
<li>
|
||||
<a href="/posts?tags={{ tag.name }}">{{ tag.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||
%}{% endif %}
|
||||
<div><a href="/">< To home</a></div>
|
||||
<div><a href="{% if let Some(tags_text) = tags_text %}/posts.xml?tags={{ tags_text.replace(' ', "+") }}{% else %}/posts.xml{% endif %}">RSS feed</a></div>
|
||||
<article>
|
||||
<h2>Search</h2>
|
||||
<form method="get" action="/posts">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@
|
|||
value="{{ application_name }}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Base URL</label>
|
||||
<input name="base_url" type="text" value="{{ base_url }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Ask for age confirmation?</label>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -92,12 +92,12 @@
|
|||
</ul>
|
||||
</article>
|
||||
{% endif %}
|
||||
<article>
|
||||
<article id="tags-list">
|
||||
<h2>Tags</h2>
|
||||
{% if tags.is_empty() %}
|
||||
<p>No tags in post. Consider adding some!</p>
|
||||
{% else %}
|
||||
<ul id="tags-list">
|
||||
<ul>
|
||||
{% for tag in tags %}
|
||||
<li>
|
||||
<a href="/posts?tags={{ tag.name }}">{{ tag.name }}</a>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue