| 9 |
9 |
|
use serde::Deserialize;
|
| 10 |
10 |
|
|
| 11 |
11 |
|
use crate::{
|
|
12 |
+ |
constants,
|
| 12 |
13 |
|
db::{self, Slug, UserId, Username},
|
| 13 |
14 |
|
error::{AppError, Result},
|
| 14 |
15 |
|
helpers,
|
| 22 |
23 |
|
.route("/u/{username}/rss", get(user_rss_feed))
|
| 23 |
24 |
|
.route("/p/{slug}/rss", get(project_rss_feed))
|
| 24 |
25 |
|
.route("/p/{slug}/blog/feed.xml", get(project_blog_rss))
|
|
26 |
+ |
.route("/changelog/feed.xml", get(changelog_rss))
|
| 25 |
27 |
|
.route("/feed/{user_id}", get(personal_feed))
|
| 26 |
28 |
|
}
|
| 27 |
29 |
|
|
| 184 |
186 |
|
.into_response())
|
| 185 |
187 |
|
}
|
| 186 |
188 |
|
|
|
189 |
+ |
/// Platform changelog RSS feed (alias for the "changelog" project blog feed).
|
|
190 |
+ |
///
|
|
191 |
+ |
/// Mirrors the `/changelog` page alias in `blog.rs`: same project lookup, same
|
|
192 |
+ |
/// rendering, so subscribers can follow the canonical `/changelog/feed.xml`
|
|
193 |
+ |
/// path instead of discovering the underlying `/p/changelog/blog/feed.xml`.
|
|
194 |
+ |
#[tracing::instrument(skip_all, name = "feeds::changelog_rss")]
|
|
195 |
+ |
async fn changelog_rss(State(state): State<AppState>) -> Result<Response> {
|
|
196 |
+ |
// `from_trusted` is safe here because CHANGELOG_PROJECT_SLUG is a
|
|
197 |
+ |
// compile-time constant (`&'static str`), not user input.
|
|
198 |
+ |
let slug = Slug::from_trusted(constants::CHANGELOG_PROJECT_SLUG.to_owned());
|
|
199 |
+ |
let db_project = db::projects::get_public_project_by_slug(&state.db, &slug)
|
|
200 |
+ |
.await?
|
|
201 |
+ |
.ok_or(AppError::NotFound)?;
|
|
202 |
+ |
|
|
203 |
+ |
let db_user = db::users::get_user_by_id(&state.db, db_project.user_id)
|
|
204 |
+ |
.await?
|
|
205 |
+ |
.ok_or(AppError::NotFound)?;
|
|
206 |
+ |
|
|
207 |
+ |
if db_user.is_sandbox {
|
|
208 |
+ |
return Err(AppError::NotFound);
|
|
209 |
+ |
}
|
|
210 |
+ |
|
|
211 |
+ |
let db_posts =
|
|
212 |
+ |
db::blog_posts::get_published_blog_posts_by_project(&state.db, db_project.id).await?;
|
|
213 |
+ |
|
|
214 |
+ |
let feed_items: Vec<FeedItem> = db_posts
|
|
215 |
+ |
.into_iter()
|
|
216 |
+ |
.map(|post| FeedItem {
|
|
217 |
+ |
title: post.title,
|
|
218 |
+ |
link: format!("{}/changelog/{}", state.config.host_url, post.slug),
|
|
219 |
+ |
description: post.body_markdown.chars().take(300).collect::<String>(),
|
|
220 |
+ |
pub_date: post.published_at.unwrap_or(post.created_at),
|
|
221 |
+ |
guid: post.id.to_string(),
|
|
222 |
+ |
})
|
|
223 |
+ |
.collect();
|
|
224 |
+ |
|
|
225 |
+ |
let xml = rss::render_blog_feed(
|
|
226 |
+ |
&db_project.title,
|
|
227 |
+ |
&db_project.slug,
|
|
228 |
+ |
db_project.description.as_deref().unwrap_or(""),
|
|
229 |
+ |
&db_user.username,
|
|
230 |
+ |
&feed_items,
|
|
231 |
+ |
&state.config.host_url,
|
|
232 |
+ |
);
|
|
233 |
+ |
|
|
234 |
+ |
Ok((
|
|
235 |
+ |
[(
|
|
236 |
+ |
axum::http::header::CONTENT_TYPE,
|
|
237 |
+ |
"application/rss+xml; charset=utf-8",
|
|
238 |
+ |
)],
|
|
239 |
+ |
xml,
|
|
240 |
+ |
)
|
|
241 |
+ |
.into_response())
|
|
242 |
+ |
}
|
|
243 |
+ |
|
| 187 |
244 |
|
/// Query parameters for the personal feed.
|
| 188 |
245 |
|
#[derive(Debug, Deserialize)]
|
| 189 |
246 |
|
struct FeedQuery {
|