Plume/src/routes/mod.rs

271 lines
7.7 KiB
Rust
Raw Normal View History

#![warn(clippy::too_many_arguments)]
2020-01-21 07:02:03 +01:00
use crate::template_utils::Ructe;
use atom_syndication::{
ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, LinkBuilder, Person, PersonBuilder,
};
use chrono::naive::NaiveDateTime;
use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
use rocket::{
http::{
hyper::header::{CacheControl, CacheDirective, ETag, EntityTag},
2019-03-20 17:56:17 +01:00
uri::{FromUriParam, Query},
RawStr, Status,
},
request::{self, FromFormValue, FromRequest, Request},
response::{self, Flash, NamedFile, Redirect, Responder, Response},
2019-03-20 17:56:17 +01:00
Outcome,
};
use std::{
collections::hash_map::DefaultHasher,
hash::Hasher,
path::{Path, PathBuf},
};
2018-05-10 20:01:16 +02:00
/// 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);
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 14:29:34 +02: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)
2020-05-04 23:46:59 +02:00
.updated(updated.format("%Y-%m-%dT%H:%M:%SZ").to_string())
.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())
.build()
.expect("Atom feed: link error")])
.build()
.expect("user::atom_feed: Error building Atom feed")
}
fn post_to_atom(post: Post, conn: &Connection) -> Entry {
let formatted_creation_date = post.creation_date.format("%Y-%m-%dT%H:%M:%SZ").to_string();
2018-09-01 22:08:26 +02:00
EntryBuilder::default()
2018-11-06 10:49:46 +01:00
.title(format!("<![CDATA[{}]]>", post.title))
2019-03-20 17:56:17 +01:00
.content(
ContentBuilder::default()
.value(format!("<![CDATA[{}]]>", *post.content.get()))
.content_type("html".to_string())
.build()
.expect("Atom feed: content error"),
)
.authors(
post.get_authors(&*conn)
.expect("Atom feed: author error")
.into_iter()
.map(|a| {
PersonBuilder::default()
.name(a.display_name)
.uri(a.ap_url)
.build()
.expect("Atom feed: author error")
})
.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)
.published(formatted_creation_date.clone())
.updated(formatted_creation_date)
.id(post.ap_url.clone())
2019-03-20 17:56:17 +01:00
.links(vec![LinkBuilder::default()
.href(post.ap_url)
.build()
.expect("Atom feed: link error")])
.build()
.expect("Atom feed: entry error")
2018-09-01 22:08:26 +02:00
}
2018-04-23 12:54:37 +02:00
pub mod blogs;
2018-05-10 11:44:57 +02:00
pub mod comments;
2018-06-18 17:59:49 +02:00
pub mod errors;
pub mod instance;
2018-05-10 18:38:03 +02:00
pub mod likes;
2018-09-02 22:55:42 +02:00
pub mod medias;
2018-05-13 15:35:55 +02:00
pub mod notifications;
2018-04-23 16:25:39 +02:00
pub mod posts;
2018-05-19 11:51:10 +02:00
pub mod reshares;
2019-03-20 17:56:17 +01:00
pub mod search;
2018-04-24 11:21:39 +02:00
pub mod session;
2018-09-06 14:06:04 +02: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 19:08:20 +02:00
pub mod timelines;
2018-04-22 20:13:12 +02:00
pub mod user;
2018-04-24 10:35:45 +02:00
pub mod well_known;
2018-05-10 20:01:16 +02:00
#[derive(Responder)]
#[response()]
pub struct CachedFile {
inner: NamedFile,
2019-03-20 17:56:17 +01:00
cache_control: CacheControl,
}
#[derive(Debug)]
pub struct ThemeFile(NamedFile);
impl<'r> Responder<'r> for ThemeFile {
2020-01-21 07:02:03 +01:00
fn respond_to(self, r: &Request<'_>) -> response::Result<'r> {
2019-08-21 21:41:11 +02:00
let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?;
let mut hasher = DefaultHasher::new();
2019-08-21 21:41:11 +02: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 21:41:11 +02:00
NamedFile::open(Path::new("static/css/").join(file))
.ok()
.map(ThemeFile)
}
#[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> {
NamedFile::open(Path::new(&CONFIG.media_directory).join(file))
.ok()
.map(|f| CachedFile {
inner: 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 17:56:17 +01:00
NamedFile::open(Path::new("static/").join(file))
.ok()
.map(|f| CachedFile {
inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
2018-05-10 20:01:16 +02:00
}