Plume/src/routes/mod.rs

302 lines
8.4 KiB
Rust
Raw Normal View History

#![warn(clippy::too_many_arguments)]
2020-01-21 06:02:03 +00:00
use crate::template_utils::Ructe;
use atom_syndication::{
ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, LinkBuilder, Person, PersonBuilder,
};
2022-01-16 03:02:12 +00:00
use chrono::{naive::NaiveDateTime, DateTime, Utc};
use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
use rocket::{
http::{
hyper::header::{CacheControl, CacheDirective, ETag, EntityTag},
2019-03-20 16:56:17 +00:00
uri::{FromUriParam, Query},
RawStr, Status,
},
request::{self, FromFormValue, FromRequest, Request},
response::{self, Flash, NamedFile, Redirect, Responder, Response},
2019-03-20 16:56:17 +00:00
Outcome,
};
use std::{
collections::hash_map::DefaultHasher,
hash::Hasher,
path::{Path, PathBuf},
};
2018-05-10 18:01:16 +00:00
#[cfg(feature = "s3")]
use rocket::http::ContentType;
/// Special return type used for routes that "cannot fail", and instead
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
#[allow(clippy::large_enum_variant)]
#[derive(Responder)]
pub enum RespondOrRedirect {
Response(Ructe),
FlashResponse(Flash<Ructe>),
Redirect(Redirect),
FlashRedirect(Flash<Redirect>),
}
impl From<Ructe> for RespondOrRedirect {
fn from(response: Ructe) -> Self {
RespondOrRedirect::Response(response)
}
}
impl From<Flash<Ructe>> for RespondOrRedirect {
fn from(response: Flash<Ructe>) -> Self {
RespondOrRedirect::FlashResponse(response)
}
}
impl From<Redirect> for RespondOrRedirect {
fn from(redirect: Redirect) -> Self {
RespondOrRedirect::Redirect(redirect)
}
}
impl From<Flash<Redirect>> for RespondOrRedirect {
fn from(redirect: Flash<Redirect>) -> Self {
RespondOrRedirect::FlashRedirect(redirect)
}
}
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
pub struct Page(i32);
2022-05-05 10:03:33 +00:00
impl From<i32> for Page {
fn from(page: i32) -> Self {
Self(page)
}
}
impl<'v> FromFormValue<'v> for Page {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
match form_value.parse::<i32>() {
Ok(page) => Ok(Page(page)),
_ => Err(form_value),
}
}
}
impl FromUriParam<Query, Option<Page>> for Page {
type Target = Page;
fn from_uri_param(val: Option<Page>) -> Page {
val.unwrap_or_default()
}
}
impl Page {
2018-07-25 12:29:34 +00:00
/// Computes the total number of pages needed to display n_items
pub fn total(n_items: i32) -> i32 {
if n_items % ITEMS_PER_PAGE == 0 {
n_items / ITEMS_PER_PAGE
} else {
(n_items / ITEMS_PER_PAGE) + 1
}
}
pub fn limits(self) -> (i32, i32) {
((self.0 - 1) * ITEMS_PER_PAGE, self.0 * ITEMS_PER_PAGE)
}
}
#[derive(Shrinkwrap)]
pub struct ContentLen(pub u64);
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
type Error = ();
fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match r.limits().get("forms") {
Some(l) => Outcome::Success(ContentLen(l)),
None => Outcome::Failure((Status::InternalServerError, ())),
}
}
}
impl Default for Page {
fn default() -> Self {
Page(1)
}
}
/// A form for remote interaction, used by multiple routes
#[derive(Shrinkwrap, Clone, Default, FromForm)]
pub struct RemoteForm {
pub remote: String,
}
pub fn build_atom_feed(
entries: Vec<Post>,
uri: &str,
title: &str,
default_updated: &NaiveDateTime,
conn: &Connection,
) -> Feed {
let updated = if entries.is_empty() {
default_updated
} else {
&entries[0].creation_date
};
FeedBuilder::default()
.title(title)
.id(uri)
2022-01-16 03:02:12 +00:00
.updated(DateTime::<Utc>::from_utc(*updated, Utc))
.entries(
entries
.into_iter()
.map(|p| post_to_atom(p, conn))
.collect::<Vec<Entry>>(),
)
.links(vec![LinkBuilder::default()
.href(uri)
.rel("self")
.mime_type("application/atom+xml".to_string())
2022-01-16 03:02:12 +00:00
.build()])
.build()
}
fn post_to_atom(post: Post, conn: &Connection) -> Entry {
2018-09-01 20:08:26 +00:00
EntryBuilder::default()
2018-11-06 09:49:46 +00:00
.title(format!("<![CDATA[{}]]>", post.title))
2019-03-20 16:56:17 +00:00
.content(
ContentBuilder::default()
.value(format!("<![CDATA[{}]]>", *post.content.get()))
.content_type("html".to_string())
2022-01-16 03:02:12 +00:00
.build(),
2019-03-20 16:56:17 +00:00
)
.authors(
2023-01-02 17:45:13 +00:00
post.get_authors(conn)
2019-03-20 16:56:17 +00:00
.expect("Atom feed: author error")
.into_iter()
.map(|a| {
PersonBuilder::default()
.name(a.display_name)
.uri(a.ap_url)
.build()
})
.collect::<Vec<Person>>(),
)
// Using RFC 4287 format, see https://tools.ietf.org/html/rfc4287#section-3.3 for dates
// eg: 2003-12-13T18:30:02Z (Z is here because there is no timezone support with the NaiveDateTime crate)
2022-01-16 03:02:12 +00:00
.published(Some(
DateTime::<Utc>::from_utc(post.creation_date, Utc).into(),
))
.updated(DateTime::<Utc>::from_utc(post.creation_date, Utc))
.id(post.ap_url.clone())
2022-01-16 03:02:12 +00:00
.links(vec![LinkBuilder::default().href(post.ap_url).build()])
2019-03-20 16:56:17 +00:00
.build()
2018-09-01 20:08:26 +00:00
}
2018-04-23 10:54:37 +00:00
pub mod blogs;
2018-05-10 09:44:57 +00:00
pub mod comments;
2022-01-06 11:18:20 +00:00
pub mod email_signups;
2018-06-18 15:59:49 +00:00
pub mod errors;
pub mod instance;
2018-05-10 16:38:03 +00:00
pub mod likes;
2018-09-02 20:55:42 +00:00
pub mod medias;
2018-05-13 13:35:55 +00:00
pub mod notifications;
2018-04-23 14:25:39 +00:00
pub mod posts;
2018-05-19 09:51:10 +00:00
pub mod reshares;
2019-03-20 16:56:17 +00:00
pub mod search;
2018-04-24 09:21:39 +00:00
pub mod session;
2018-09-06 12:06:04 +00:00
pub mod tags;
Add support for generic timeline (#525) * Begin adding support for timeline * fix some bugs with parser * fmt * add error reporting for parser * add tests for timeline query parser * add rejection tests for parse * begin adding support for lists also run migration before compiling, so schema.rs is up to date * add sqlite migration * end adding lists still miss tests and query integration * cargo fmt * try to add some tests * Add some constraint to db, and fix list test and refactor other tests to use begin_transaction * add more tests for lists * add support for lists in query executor * add keywords for including/excluding boosts and likes * cargo fmt * add function to list lists used by query will make it easier to warn users when creating timeline with unknown lists * add lang support * add timeline creation error message when using unexisting lists * Update .po files * WIP: interface for timelines * don't use diesel for migrations not sure how it passed the ci on the other branch * add some tests for timeline add an int representing the order of timelines (first one will be on top, second just under...) use first() instead of limit(1).get().into_iter().nth(0) remove migrations from build artifacts as they are now compiled in * cargo fmt * remove timeline order * fix tests * add tests for timeline creation failure * cargo fmt * add tests for timelines * add test for matching direct lists and keywords * add test for language filtering * Add a more complex test for Timeline::matches, and fix TQ::matches for TQ::Or * Make the main crate compile + FMT * Use the new timeline system - Replace the old "feed" system with timelines - Display all timelines someone can access on their home page (either their personal ones, or instance timelines) - Remove functions that were used to get user/local/federated feed - Add new posts to timelines - Create a default timeline called "My feed" for everyone, and "Local feed"/"Federated feed" with timelines @fdb-hiroshima I don't know if that's how you pictured it? If you imagined it differently I can of course make changes. I hope I didn't forgot anything… * Cargo fmt * Try to fix the migration * Fix tests * Fix the test (for real this time ?) * Fix the tests ? + fmt * Use Kind::Like and Kind::Reshare when needed * Forgot to run cargo fmt once again * revert translations * fix reviewed stuff * reduce code duplication by macros * cargo fmt
2019-10-07 17:08:20 +00:00
pub mod timelines;
2018-04-22 18:13:12 +00:00
pub mod user;
2018-04-24 08:35:45 +00:00
pub mod well_known;
2018-05-10 18:01:16 +00:00
2022-11-13 10:18:13 +00:00
#[derive(Responder)]
enum FileKind {
Local(NamedFile),
#[cfg(feature = "s3")]
2022-11-13 10:18:13 +00:00
S3(Vec<u8>, ContentType),
}
#[derive(Responder)]
#[response()]
pub struct CachedFile {
2022-11-13 10:18:13 +00:00
inner: FileKind,
2019-03-20 16:56:17 +00:00
cache_control: CacheControl,
}
#[derive(Debug)]
pub struct ThemeFile(NamedFile);
impl<'r> Responder<'r> for ThemeFile {
2020-01-21 06:02:03 +00:00
fn respond_to(self, r: &Request<'_>) -> response::Result<'r> {
2019-08-21 19:41:11 +00:00
let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?;
let mut hasher = DefaultHasher::new();
2019-08-21 19:41:11 +00:00
hasher.write(&contents);
let etag = format!("{:x}", hasher.finish());
if r.headers()
.get("If-None-Match")
.any(|s| s[1..s.len() - 1] == etag)
{
Response::build()
.status(Status::NotModified)
.header(ETag(EntityTag::strong(etag)))
.ok()
} else {
Response::build()
.merge(self.0.respond_to(r)?)
.header(ETag(EntityTag::strong(etag)))
.ok()
}
}
}
#[get("/static/cached/<_build_id>/css/<file..>", rank = 1)]
pub fn theme_files(file: PathBuf, _build_id: &RawStr) -> Option<ThemeFile> {
2019-08-21 19:41:11 +00:00
NamedFile::open(Path::new("static/css/").join(file))
.ok()
.map(ThemeFile)
}
#[allow(unused_variables)]
#[get("/static/cached/<build_id>/<file..>", rank = 2)]
pub fn plume_static_files(file: PathBuf, build_id: &RawStr) -> Option<CachedFile> {
static_files(file)
}
#[get("/static/media/<file..>")]
pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
if CONFIG.s3.is_some() {
#[cfg(not(feature="s3"))]
unreachable!();
#[cfg(feature="s3")]
{
let ct = file.extension()
.and_then(|ext| ContentType::from_extension(&ext.to_string_lossy()))
.unwrap_or(ContentType::Binary);
2022-11-13 10:18:13 +00:00
let data = CONFIG.s3.as_ref().unwrap().get_bucket()
.get_object_blocking(format!("static/media/{}", file.to_string_lossy())).ok()?;
2022-11-13 10:18:13 +00:00
Some(CachedFile {
inner: FileKind::S3 ( data.to_vec(), ct),
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
}
2022-11-13 10:18:13 +00:00
} else {
NamedFile::open(Path::new(&CONFIG.media_directory).join(file))
.ok()
.map(|f| CachedFile {
inner: FileKind::Local(f),
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
}
}
#[get("/static/<file..>", rank = 3)]
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
2019-03-20 16:56:17 +00:00
NamedFile::open(Path::new("static/").join(file))
.ok()
.map(|f| CachedFile {
2022-11-13 10:18:13 +00:00
inner: FileKind::Local(f),
2019-03-20 16:56:17 +00:00
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
2018-05-10 18:01:16 +00:00
}