Compare commits

..

2 Commits

Author SHA1 Message Date
Violet white 26867ca6be Retrieve signing user from database instead of expecting it to materialize from thin air 2020-01-14 13:56:51 -05:00
Violet white 2b4e802914 Work towards authorized fetch 2020-01-12 22:09:56 -05:00
136 changed files with 3381 additions and 3479 deletions
+1 -2
View File
@@ -10,7 +10,7 @@ executors:
type: boolean type: boolean
default: false default: false
docker: docker:
- image: plumeorg/plume-buildenv:v0.0.9 - image: plumeorg/plume-buildenv:v0.0.8
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>> - image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
@@ -226,7 +226,6 @@ jobs:
steps: steps:
- restore_env: - restore_env:
cache: none cache: none
- run: cargo build
- run: crowdin upload -b master - run: crowdin upload -b master
workflows: workflows:
+1 -1
View File
@@ -8,7 +8,7 @@ RUN apt update &&\
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
#install and configure rust #install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-01-15 -y &&\ RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-03-23 -y &&\
rustup component add rustfmt clippy &&\ rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown rustup component add rust-std --target wasm32-unknown-unknown
-1
View File
@@ -18,4 +18,3 @@ tags.*
search_index search_index
.buildconfig .buildconfig
__pycache__ __pycache__
.vscode/
Generated
+2088 -2028
View File
File diff suppressed because it is too large Load Diff
+11 -14
View File
@@ -3,12 +3,10 @@ authors = ["Plume contributors"]
name = "plume" name = "plume"
version = "0.4.0" version = "0.4.0"
repository = "https://github.com/Plume-org/Plume" repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.3" activitypub = "0.1.3"
askama_escape = "0.1" askama_escape = "0.1"
async-trait = "*"
atom_syndication = "0.6" atom_syndication = "0.6"
clap = "2.33" clap = "2.33"
colored = "1.8" colored = "1.8"
@@ -21,19 +19,20 @@ heck = "0.3.0"
lettre = "0.9.2" lettre = "0.9.2"
lettre_email = "0.9.2" lettre_email = "0.9.2"
num_cpus = "1.10" num_cpus = "1.10"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" } rocket = "0.4.0"
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" , features = ["json"] } rocket_contrib = { version = "0.4.0", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0" rpassword = "4.0"
runtime-fmt = "0.3.0"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_qs = "0.5" serde_qs = "0.5"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.2.1"
syntect = "3.3" syntect = "3.3"
tokio = "0.2" validator = "0.8"
validator = "0.10" validator_derive = "0.8"
validator_derive = "0.10" webfinger = "0.4.1"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] }
[[bin]] [[bin]]
name = "plume" name = "plume"
@@ -65,14 +64,12 @@ path = "plume-common"
[dependencies.plume-models] [dependencies.plume-models]
path = "plume-models" path = "plume-models"
[dependencies.rocket_i18n] [dependencies.rocket_csrf]
git = "https://github.com/Plume-org/rocket_i18n" git = "https://github.com/fdb-hiroshima/rocket_csrf"
branch = "go-async" rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
default-features = false
features = ["rocket"]
[build-dependencies] [build-dependencies]
ructe = "0.9.0" ructe = "0.6.2"
rsass = "0.9" rsass = "0.9"
[features] [features]
+1 -2
View File
@@ -10,8 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \ gcc \
make \ make \
openssl \ openssl \
libssl-dev \ libssl-dev
clang
WORKDIR /scratch WORKDIR /scratch
COPY script/wasm-deps.sh . COPY script/wasm-deps.sh .
+88 -22
View File
@@ -190,28 +190,7 @@ p.error {
background: $gray; background: $gray;
text-overflow: ellipsis; text-overflow: ellipsis;
footer.authors {
div {
float: left;
margin-right: 0.25em;
}
.likes { color: $red; }
.reshares { color: $primary; }
span.likes, span.resahres {
font-family: "Route159",serif;
font-size: 1em;
}
svg.feather {
width: 0.85em;
height: 0.85em;
}
}
> * { > * {
margin: 20px; margin: 20px;
@@ -490,6 +469,93 @@ figure {
/// Small screens /// Small screens
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, #content.show {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
main .article-meta { main .article-meta {
> *, .comments { > *, .comments {
margin: 0 5%; margin: 0 5%;
-90
View File
@@ -101,96 +101,6 @@ body > header {
} }
} }
/// Small screens
@media screen and (max-width: 600px) {
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, #content.show {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
}
/* Only enable label animations on large screens */ /* Only enable label animations on large screens */
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
header nav a { header nav a {
+3 -1
View File
@@ -1,6 +1,8 @@
extern crate rsass;
extern crate ructe;
use ructe::Ructe; use ructe::Ructe;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::{ffi::OsStr, fs::*, io::Write, path::*}; use std::{env, ffi::OsStr, fs::*, io::Write, path::*};
fn compute_static_hash() -> String { fn compute_static_hash() -> String {
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r" //"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r"
-1
View File
@@ -2,7 +2,6 @@
name = "plume-api" name = "plume-api"
version = "0.4.0" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018"
[dependencies] [dependencies]
serde = "1.0" serde = "1.0"
+1
View File
@@ -1,3 +1,4 @@
extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
-1
View File
@@ -2,7 +2,6 @@
name = "plume-cli" name = "plume-cli"
version = "0.4.0" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018"
[[bin]] [[bin]]
name = "plm" name = "plm"
+5 -1
View File
@@ -1,4 +1,8 @@
use dotenv; extern crate clap;
extern crate diesel;
extern crate dotenv;
extern crate plume_models;
extern crate rpassword;
use clap::App; use clap::App;
use diesel::Connection; use diesel::Connection;
+5 -11
View File
@@ -2,27 +2,25 @@
name = "plume-common" name = "plume-common"
version = "0.4.0" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
activitystreams-derive = "0.2" activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0" activitystreams-traits = "0.1.0"
array_tool = "1.0" array_tool = "1.0"
base64 = "0.10" base64 = "0.10"
futures-util = "*"
heck = "0.3.0" heck = "0.3.0"
hex = "0.3" hex = "0.3"
hyper = "0.13" hyper = "0.12.33"
openssl = "0.10.22" openssl = "0.10.22"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" } rocket = "0.4.0"
reqwest = "0.9"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.2.1"
syntect = "3.3" syntect = "3.3"
tokio = "0.2" tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -31,7 +29,3 @@ version = "0.4"
[dependencies.pulldown-cmark] [dependencies.pulldown-cmark]
default-features = false default-features = false
version = "0.2.0" version = "0.2.0"
[dependencies.reqwest]
features = ["json", "blocking"]
version = "0.10"
+3 -4
View File
@@ -279,9 +279,8 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID /// Dereferences an ID
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> { fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
// Use blocking reqwest API here, since defer cannot be async (yet) reqwest::ClientBuilder::new()
reqwest::blocking::Client::builder() .connect_timeout(Some(std::time::Duration::from_secs(5)))
.connect_timeout(std::time::Duration::from_secs(5))
.build() .build()
.map_err(|_| (None, InboxError::DerefError.into()))? .map_err(|_| (None, InboxError::DerefError.into()))?
.get(id) .get(id)
@@ -297,7 +296,7 @@ pub trait FromId<C>: Sized {
) )
.send() .send()
.map_err(|_| (None, InboxError::DerefError)) .map_err(|_| (None, InboxError::DerefError))
.and_then(|r| { .and_then(|mut r| {
let json: serde_json::Value = r let json: serde_json::Value = r
.json() .json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?; .map_err(|_| (None, InboxError::InvalidObject(None)))?;
+37 -42
View File
@@ -1,12 +1,14 @@
use activitypub::{Activity, Link, Object}; use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use reqwest::ClientBuilder; use reqwest::r#async::ClientBuilder;
use rocket::{ use rocket::{
http::Status, http::Status,
request::{FromRequest, Request}, request::{FromRequest, Request},
response::{Responder, Response, Result}, response::{Responder, Response},
Outcome, Outcome,
}; };
use serde_json;
use tokio::prelude::*;
use self::sign::Signable; use self::sign::Signable;
@@ -60,45 +62,39 @@ impl<T> ActivityStream<T> {
ActivityStream(t) ActivityStream(t)
} }
} }
#[rocket::async_trait]
impl<'r, O: Object + Send + 'r> Responder<'r> for ActivityStream<O> { impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
async fn respond_to(self, request: &'r Request<'_>) -> Result<'r> { fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context(); json["@context"] = context();
let result = serde_json::to_string(&json).map_err(rocket::response::Debug); serde_json::to_string(&json).respond_to(request).map(|r| {
match result.respond_to(request).await { Response::build_from(r)
Ok(r) => Response::build_from(r)
.raw_header("Content-Type", "application/activity+json") .raw_header("Content-Type", "application/activity+json")
.ok(), .finalize()
Err(e) => Err(e), })
}
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct ApRequest; pub struct ApRequest;
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ApRequest { impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> { fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
request request
.headers() .headers()
.get_one("Accept") .get_one("Accept")
.map(|header| { .map(|header| {
header header
.split(',') .split(',')
.map(|ct| { .map(|ct| match ct.trim() {
match ct.trim() { // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), "application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
// false otherwise | "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" | "application/activity+json"
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" | "application/ld+json" => Outcome::Success(ApRequest),
| "application/activity+json" "text/html" => Outcome::Forward(true),
| "application/ld+json" => Outcome::Success(ApRequest), _ => Outcome::Forward(false),
"text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false),
}
}) })
.fold(Outcome::Forward(false), |out, ct| { .fold(Outcome::Forward(false), |out, ct| {
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) { if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
@@ -134,38 +130,36 @@ where
.sign(sender) .sign(sender)
.expect("activity_pub::broadcast: signature error"); .expect("activity_pub::broadcast: signature error");
let rt = tokio::runtime::Builder::new() let mut rt = tokio::runtime::current_thread::Runtime::new()
.threaded_scheduler()
.build()
.expect("Error while initializing tokio runtime for federation"); .expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
for inbox in boxes { for inbox in boxes {
let body = signed.to_string(); let body = signed.to_string();
let mut headers = request::headers(); let mut headers = request::headers();
headers.insert("Digest", request::Digest::digest(&body)); headers.insert("Digest", request::Digest::digest(&body));
let sig = request::signature(sender, &headers) rt.spawn(
.expect("activity_pub::broadcast: request signature error");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
rt.spawn(async move {
client client
.post(&inbox) .post(&inbox)
.headers(headers.clone()) .headers(headers.clone())
.header("Signature", sig) .header(
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body) .body(body)
.send() .send()
.await .and_then(|r| r.into_body().concat2())
.unwrap()
.text()
.await
.map(move |response| { .map(move |response| {
println!("Successfully sent activity to inbox ({})", inbox); println!("Successfully sent activity to inbox ({})", inbox);
println!("Response: \"{:?}\"\n", response) println!("Response: \"{:?}\"\n", response)
}) })
.map_err(|e| println!("Error while sending to inbox ({:?})", e)) .map_err(|e| println!("Error while sending to inbox ({:?})", e)),
}); );
} }
rt.run().unwrap();
} }
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)] #[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
@@ -209,7 +203,8 @@ pub struct PublicKey {
pub public_key_pem: Option<serde_json::Value>, pub public_key_pem: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, UnitString)]
#[activitystreams(Hashtag)]
pub struct HashtagType; pub struct HashtagType;
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
+3 -2
View File
@@ -1,11 +1,12 @@
use base64;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use std::ops::Deref; use std::ops::Deref;
use std::time::SystemTime; use std::time::SystemTime;
use crate::activity_pub::sign::Signer; use activity_pub::sign::Signer;
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE}; use activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION")); const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
+4 -1
View File
@@ -1,7 +1,10 @@
use super::request; use super::request;
use base64;
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc}; use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
use hex;
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256}; use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
use rocket::http::HeaderMap; use rocket::http::HeaderMap;
use serde_json;
/// Returns (public key, private key) /// Returns (public key, private key)
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) { pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
@@ -128,7 +131,7 @@ impl SignatureValidity {
pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>( pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
sender: &S, sender: &S,
all_headers: &HeaderMap<'_>, all_headers: &HeaderMap,
data: &request::Digest, data: &request::Digest,
) -> SignatureValidity { ) -> SignatureValidity {
let sig_header = all_headers.get_one("Signature"); let sig_header = all_headers.get_one("Signature");
+15 -2
View File
@@ -1,14 +1,27 @@
#![feature(associated_type_defaults)] #![feature(custom_attribute, associated_type_defaults)]
extern crate activitypub;
#[macro_use] #[macro_use]
extern crate activitystreams_derive; extern crate activitystreams_derive;
extern crate activitystreams_traits;
extern crate array_tool;
extern crate base64;
extern crate chrono;
extern crate heck;
extern crate hex;
extern crate openssl;
extern crate pulldown_cmark;
extern crate reqwest;
extern crate rocket;
extern crate serde;
#[macro_use] #[macro_use]
extern crate shrinkwraprs; extern crate shrinkwraprs;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
extern crate syntect;
extern crate tokio;
pub mod activity_pub; pub mod activity_pub;
pub mod utils; pub mod utils;
+14 -32
View File
@@ -1,7 +1,6 @@
use heck::CamelCase; use heck::CamelCase;
use openssl::rand::rand_bytes; use openssl::rand::rand_bytes;
use pulldown_cmark::{html, Event, Options, Parser, Tag}; use pulldown_cmark::{html, Event, Options, Parser, Tag};
use regex_syntax::is_word_character;
use rocket::{ use rocket::{
http::uri::Uri, http::uri::Uri,
response::{Flash, Redirect}, response::{Flash, Redirect},
@@ -49,7 +48,7 @@ enum State {
Ready, Ready,
} }
fn to_inline(tag: Tag<'_>) -> Tag<'_> { fn to_inline(tag: Tag) -> Tag {
match tag { match tag {
Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => { Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
Tag::Paragraph Tag::Paragraph
@@ -147,7 +146,7 @@ fn inline_tags<'a>(
} }
} }
pub type MediaProcessor<'a> = Box<dyn 'a + Fn(i32) -> Option<(String, Option<String>)>>; pub type MediaProcessor<'a> = Box<'a + Fn(i32) -> Option<(String, Option<String>)>>;
fn process_image<'a, 'b>( fn process_image<'a, 'b>(
evt: Event<'a>, evt: Event<'a>,
@@ -158,7 +157,9 @@ fn process_image<'a, 'b>(
match evt { match evt {
Event::Start(Tag::Image(id, title)) => { Event::Start(Tag::Image(id, title)) => {
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) { if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
if let (Some(cw), false) = (cw, inline) { if inline || cw.is_none() {
Event::Start(Tag::Image(Cow::Owned(url), title))
} else {
// there is a cw, and where are not inline // there is a cw, and where are not inline
Event::Html(Cow::Owned(format!( Event::Html(Cow::Owned(format!(
r#"<label for="postcontent-cw-{id}"> r#"<label for="postcontent-cw-{id}">
@@ -169,11 +170,9 @@ fn process_image<'a, 'b>(
</span> </span>
<img src="{url}" alt=""#, <img src="{url}" alt=""#,
id = random_hex(), id = random_hex(),
cw = cw, cw = cw.unwrap(),
url = url url = url
))) )))
} else {
Event::Start(Tag::Image(Cow::Owned(url), title))
} }
} else { } else {
Event::Start(Tag::Image(id, title)) Event::Start(Tag::Image(id, title))
@@ -201,12 +200,6 @@ fn process_image<'a, 'b>(
} }
} }
#[derive(Default, Debug)]
struct DocumentContext {
in_code: bool,
in_link: bool,
}
/// Returns (HTML, mentions, hashtags) /// Returns (HTML, mentions, hashtags)
pub fn md_to_html<'a>( pub fn md_to_html<'a>(
md: &str, md: &str,
@@ -221,7 +214,7 @@ pub fn md_to_html<'a>(
}; };
let parser = Parser::new_ext(md, Options::all()); let parser = Parser::new_ext(md, Options::all());
let (parser, mentions, hashtags): (Vec<Event<'_>>, Vec<String>, Vec<String>) = parser let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
// Flatten text because pulldown_cmark break #hashtag in two individual text elements // Flatten text because pulldown_cmark break #hashtag in two individual text elements
.scan(None, flatten_text) .scan(None, flatten_text)
.flatten() .flatten()
@@ -230,21 +223,13 @@ pub fn md_to_html<'a>(
.map(|evt| process_image(evt, inline, &media_processor)) .map(|evt| process_image(evt, inline, &media_processor))
// Ignore headings, images, and tables if inline = true // Ignore headings, images, and tables if inline = true
.scan((vec![], inline), inline_tags) .scan((vec![], inline), inline_tags)
.scan(&mut DocumentContext::default(), |ctx, evt| match evt { .scan(false, |in_code, evt| match evt {
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => { Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
ctx.in_code = true; *in_code = true;
Some((vec![evt], vec![], vec![])) Some((vec![evt], vec![], vec![]))
} }
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => { Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
ctx.in_code = false; *in_code = false;
Some((vec![evt], vec![], vec![]))
}
Event::Start(Tag::Link(_, _)) => {
ctx.in_link = true;
Some((vec![evt], vec![], vec![]))
}
Event::End(Tag::Link(_, _)) => {
ctx.in_link = false;
Some((vec![evt], vec![], vec![])) Some((vec![evt], vec![], vec![]))
} }
Event::Text(txt) => { Event::Text(txt) => {
@@ -262,7 +247,7 @@ pub fn md_to_html<'a>(
text_acc.push(c) text_acc.push(c)
} }
let mention = text_acc; let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or(""); let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
let link = Tag::Link( let link = Tag::Link(
format!("{}@/{}/", base_url, &mention).into(), format!("{}@/{}/", base_url, &mention).into(),
short_mention.to_owned().into(), short_mention.to_owned().into(),
@@ -284,7 +269,7 @@ pub fn md_to_html<'a>(
} }
} }
State::Hashtag => { State::Hashtag => {
let char_matches = c == '-' || is_word_character(c); let char_matches = c.is_alphanumeric() || "-_".contains(c);
if char_matches && (n < (txt.chars().count() - 1)) { if char_matches && (n < (txt.chars().count() - 1)) {
text_acc.push(c); text_acc.push(c);
(events, State::Hashtag, text_acc, n + 1, mentions, hashtags) (events, State::Hashtag, text_acc, n + 1, mentions, hashtags)
@@ -315,7 +300,7 @@ pub fn md_to_html<'a>(
} }
} }
State::Ready => { State::Ready => {
if !ctx.in_code && !ctx.in_link && c == '@' { if !*in_code && c == '@' {
events.push(Event::Text(text_acc.into())); events.push(Event::Text(text_acc.into()));
( (
events, events,
@@ -325,7 +310,7 @@ pub fn md_to_html<'a>(
mentions, mentions,
hashtags, hashtags,
) )
} else if !ctx.in_code && !ctx.in_link && c == '#' { } else if !*in_code && c == '#' {
events.push(Event::Text(text_acc.into())); events.push(Event::Text(text_acc.into()));
( (
events, events,
@@ -414,7 +399,6 @@ mod tests {
("not_a@mention", vec![]), ("not_a@mention", vec![]),
("`@helo`", vec![]), ("`@helo`", vec![]),
("```\n@hello\n```", vec![]), ("```\n@hello\n```", vec![]),
("[@atmark in link](https://example.org/)", vec![]),
]; ];
for (md, mentions) in tests { for (md, mentions) in tests {
@@ -440,8 +424,6 @@ mod tests {
("with some punctuation #test!", vec!["test"]), ("with some punctuation #test!", vec!["test"]),
(" #spaces ", vec!["spaces"]), (" #spaces ", vec!["spaces"]),
("not_a#hashtag", vec![]), ("not_a#hashtag", vec![]),
("#نرم‌افزار_آزاد", vec!["نرم‌افزار_آزاد"]),
("[#hash in link](https://example.org/)", vec![]),
]; ];
for (md, mentions) in tests { for (md, mentions) in tests {
-1
View File
@@ -2,7 +2,6 @@
name = "plume-front" name = "plume-front"
version = "0.4.0" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018"
[dependencies] [dependencies]
stdweb = "=0.4.18" stdweb = "=0.4.18"
+1 -1
View File
@@ -1,4 +1,3 @@
use crate::CATALOG;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json; use serde_json;
use std::sync::Mutex; use std::sync::Mutex;
@@ -6,6 +5,7 @@ use stdweb::{
unstable::{TryFrom, TryInto}, unstable::{TryFrom, TryInto},
web::{event::*, html_element::*, *}, web::{event::*, html_element::*, *},
}; };
use CATALOG;
macro_rules! mv { macro_rules! mv {
( $( $var:ident ),* => $exp:expr ) => { ( $( $var:ident ),* => $exp:expr ) => {
+3
View File
@@ -1,12 +1,15 @@
#![recursion_limit = "128"] #![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
extern crate gettext;
#[macro_use] #[macro_use]
extern crate gettext_macros; extern crate gettext_macros;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate stdweb; extern crate stdweb;
extern crate serde;
extern crate serde_json;
use stdweb::web::{event::*, *}; use stdweb::web::{event::*, *};
init_i18n!( init_i18n!(
+14 -11
View File
@@ -1,7 +1,8 @@
#![recursion_limit = "128"] #![recursion_limit = "128"]
extern crate proc_macro;
#[macro_use] #[macro_use]
extern crate quote; extern crate quote;
extern crate syn;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
@@ -102,17 +103,19 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push_str(line); acc.push_str(line);
acc.push('\n'); acc.push('\n');
} }
} else if line.starts_with("--#!") {
acc.push_str(&line[4..]);
acc.push('\n');
} else if line.starts_with("--") {
continue;
} else { } else {
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into()); if line.starts_with("--#!") {
actions.push(quote!(Action::Function(&#func))); acc.push_str(&line[4..]);
sql = true; acc.push('\n');
acc = line.to_string(); } else if line.starts_with("--") {
acc.push('\n'); continue;
} else {
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
actions.push(quote!(Action::Function(&#func)));
sql = true;
acc = line.to_string();
acc.push('\n');
}
} }
} }
if !acc.trim().is_empty() { if !acc.trim().is_empty() {
+7 -19
View File
@@ -2,7 +2,6 @@
name = "plume-models" name = "plume-models"
version = "0.4.0" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
@@ -10,24 +9,24 @@ ammonia = "2.1.1"
askama_escape = "0.1" askama_escape = "0.1"
bcrypt = "0.5" bcrypt = "0.5"
guid-create = "0.1" guid-create = "0.1"
futures = "0.3"
heck = "0.3.0" heck = "0.3.0"
itertools = "0.8.0" itertools = "0.8.0"
lazy_static = "1.0" lazy_static = "*"
migrations_internals= "1.4.0" migrations_internals= "1.4.0"
openssl = "0.10.22" openssl = "0.10.22"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" } rocket = "0.4.0"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
tantivy = "0.10.1" tantivy = "0.10.1"
tokio = "0.2"
url = "2.1" url = "2.1"
walkdir = "2.2" walkdir = "2.2"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] } webfinger = "0.4.1"
whatlang = "0.7.1" whatlang = "0.7.1"
shrinkwraprs = "0.3" shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2" diesel-derive-newtype = "0.1.2"
glob = "0.3.0" glob = "0.3.0"
@@ -48,19 +47,8 @@ path = "../plume-common"
[dependencies.plume-macro] [dependencies.plume-macro]
path = "../plume-macro" path = "../plume-macro"
[dependencies.reqwest]
features = ["json", "blocking"]
version = "0.10"
[dependencies.rocket_i18n]
git = "https://github.com/Plume-org/rocket_i18n"
branch = "go-async"
default-features = false
features = ["rocket"]
[dev-dependencies] [dev-dependencies]
diesel_migrations = "1.4.0" diesel_migrations = "1.3.0"
[features] [features]
postgres = ["diesel/postgres", "plume-macro/postgres" ] postgres = ["diesel/postgres", "plume-macro/postgres" ]
+6 -7
View File
@@ -1,19 +1,19 @@
use crate::users::User;
use rocket::{ use rocket::{
http::Status, http::Status,
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
Outcome, Outcome,
}; };
use users::User;
/// Wrapper around User to use as a request guard on pages reserved to admins. /// Wrapper around User to use as a request guard on pages reserved to admins.
pub struct Admin(pub User); pub struct Admin(pub User);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Admin { impl<'a, 'r> FromRequest<'a, 'r> for Admin {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
let user = try_outcome!(User::from_request(request).await); let user = request.guard::<User>()?;
if user.is_admin() { if user.is_admin() {
Outcome::Success(Admin(user)) Outcome::Success(Admin(user))
} else { } else {
@@ -25,12 +25,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
/// Same as `Admin` but for moderators. /// Same as `Admin` but for moderators.
pub struct Moderator(pub User); pub struct Moderator(pub User);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Moderator { impl<'a, 'r> FromRequest<'a, 'r> for Moderator {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Moderator, ()> {
let user = try_outcome!(User::from_request(request).await); let user = request.guard::<User>()?;
if user.is_moderator() { if user.is_moderator() {
Outcome::Success(Moderator(user)) Outcome::Success(Moderator(user))
} else { } else {
+20 -21
View File
@@ -1,4 +1,3 @@
use crate::{db_conn::DbConn, schema::api_tokens, Error, Result};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::{ use rocket::{
@@ -7,6 +6,10 @@ use rocket::{
Outcome, Outcome,
}; };
use db_conn::DbConn;
use schema::api_tokens;
use {Error, Result};
#[derive(Clone, Queryable)] #[derive(Clone, Queryable)]
pub struct ApiToken { pub struct ApiToken {
pub id: i32, pub id: i32,
@@ -76,36 +79,32 @@ pub enum TokenError {
DbError, DbError,
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ApiToken { impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
type Error = TokenError; type Error = TokenError;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, TokenError> {
let headers: Vec<_> = request.headers().get("Authorization").collect(); let headers: Vec<_> = request.headers().get("Authorization").collect();
if headers.len() != 1 { if headers.len() != 1 {
return Outcome::Failure((Status::BadRequest, TokenError::NoHeader)); return Outcome::Failure((Status::BadRequest, TokenError::NoHeader));
} }
let mut parsed_header = headers[0].split(' '); let mut parsed_header = headers[0].split(' ');
if let Some(auth_type) = parsed_header.next() { let auth_type = parsed_header.next().map_or_else(
if let Some(val) = parsed_header.next() { || Outcome::Failure((Status::BadRequest, TokenError::NoType)),
if auth_type == "Bearer" { Outcome::Success,
if let Outcome::Success(conn) = DbConn::from_request(request).await { )?;
if let Ok(token) = ApiToken::find_by_value(&*conn, val) { let val = parsed_header.next().map_or_else(
return Outcome::Success(token); || Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
} Outcome::Success,
} else { )?;
return Outcome::Failure((
Status::InternalServerError, if auth_type == "Bearer" {
TokenError::DbError, let conn = request
)); .guard::<DbConn>()
} .map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
} if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
} else { return Outcome::Success(token);
return Outcome::Failure((Status::BadRequest, TokenError::NoValue));
} }
} else {
return Outcome::Failure((Status::BadRequest, TokenError::NoType));
} }
Outcome::Forward(()) Outcome::Forward(())
+3 -1
View File
@@ -1,7 +1,9 @@
use crate::{schema::apps, Error, Result};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::apps;
use {Error, Result};
#[derive(Clone, Queryable, Serialize)] #[derive(Clone, Queryable, Serialize)]
pub struct App { pub struct App {
pub id: i32, pub id: i32,
+6 -3
View File
@@ -1,7 +1,9 @@
use crate::{schema::email_blocklist, Connection, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, TextExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, TextExpressionMethods};
use glob::Pattern; use glob::Pattern;
use schema::email_blocklist;
use {Connection, Error, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
#[table_name = "email_blocklist"] #[table_name = "email_blocklist"]
pub struct BlocklistedEmail { pub struct BlocklistedEmail {
@@ -87,9 +89,10 @@ impl BlocklistedEmail {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{instance::tests as instance_tests, tests::rockets, Connection as Conn};
use diesel::Connection; use diesel::Connection;
use instance::tests as instance_tests;
use tests::rockets;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> { pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
let domainblock = let domainblock =
+3 -1
View File
@@ -1,6 +1,8 @@
use crate::{schema::blog_authors, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::blog_authors;
use {Error, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct BlogAuthor { pub struct BlogAuthor {
pub id: i32, pub id: i32,
+28 -26
View File
@@ -1,7 +1,3 @@
use crate::{
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
};
use activitypub::{ use activitypub::{
actor::Group, actor::Group,
collection::{OrderedCollection, OrderedCollectionPage}, collection::{OrderedCollection, OrderedCollectionPage},
@@ -16,12 +12,22 @@ use openssl::{
rsa::Rsa, rsa::Rsa,
sign::{Signer, Verifier}, sign::{Signer, Verifier},
}; };
use serde_json;
use url::Url;
use webfinger::*;
use instance::*;
use medias::Media;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, FromId}, inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source, sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
}; };
use url::Url; use posts::Post;
use webfinger::*; use safe_string::SafeString;
use schema::blogs;
use search::Searcher;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE};
pub type CustomGroup = CustomObject<ApSignature, Group>; pub type CustomGroup = CustomObject<ApSignature, Group>;
@@ -70,8 +76,7 @@ impl Blog {
insert!(blogs, NewBlog, |inserted, conn| { insert!(blogs, NewBlog, |inserted, conn| {
let instance = inserted.get_instance(conn)?; let instance = inserted.get_instance(conn)?;
if inserted.outbox_url.is_empty() { if inserted.outbox_url.is_empty() {
inserted.outbox_url = inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
instance.compute_box(BLOG_PREFIX, &inserted.actor_id, r#"outbox"#);
} }
if inserted.inbox_url.is_empty() { if inserted.inbox_url.is_empty() {
@@ -101,8 +106,8 @@ impl Blog {
} }
pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> { pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::blog_authors; use schema::blog_authors;
use crate::schema::users; use schema::users;
let authors_ids = blog_authors::table let authors_ids = blog_authors::table
.filter(blog_authors::blog_id.eq(self.id)) .filter(blog_authors::blog_id.eq(self.id))
.select(blog_authors::author_id); .select(blog_authors::author_id);
@@ -113,7 +118,7 @@ impl Blog {
} }
pub fn count_authors(&self, conn: &Connection) -> Result<i64> { pub fn count_authors(&self, conn: &Connection) -> Result<i64> {
use crate::schema::blog_authors; use schema::blog_authors;
blog_authors::table blog_authors::table
.filter(blog_authors::blog_id.eq(self.id)) .filter(blog_authors::blog_id.eq(self.id))
.count() .count()
@@ -122,7 +127,7 @@ impl Blog {
} }
pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> { pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> {
use crate::schema::blog_authors; use schema::blog_authors;
let author_ids = blog_authors::table let author_ids = blog_authors::table
.filter(blog_authors::author_id.eq(author.id)) .filter(blog_authors::author_id.eq(author.id))
.select(blog_authors::blog_id); .select(blog_authors::blog_id);
@@ -132,7 +137,7 @@ impl Blog {
.map_err(Error::from) .map_err(Error::from)
} }
pub async fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> { pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
let from_db = blogs::table let from_db = blogs::table
.filter(blogs::fqn.eq(fqn)) .filter(blogs::fqn.eq(fqn))
.first(&*c.conn) .first(&*c.conn)
@@ -140,13 +145,12 @@ impl Blog {
if let Some(from_db) = from_db { if let Some(from_db) = from_db {
Ok(from_db) Ok(from_db)
} else { } else {
Blog::fetch_from_webfinger(c, fqn).await Blog::fetch_from_webfinger(c, fqn)
} }
} }
async fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> { fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true) resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
.await?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
@@ -497,16 +501,14 @@ impl NewBlog {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use blog_authors::*;
blog_authors::*,
instance::tests as instance_tests,
medias::NewMedia,
search::tests::get_searcher,
tests::{db, rockets},
users::tests as usersTests,
Connection as Conn,
};
use diesel::Connection; use diesel::Connection;
use instance::tests as instance_tests;
use medias::NewMedia;
use search::tests::get_searcher;
use tests::{db, rockets};
use users::tests as usersTests;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) { pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
+5 -1
View File
@@ -1,6 +1,10 @@
use crate::{comments::Comment, schema::comment_seers, users::User, Connection, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use comments::Comment;
use schema::comment_seers;
use users::User;
use {Connection, Error, Result};
#[derive(Queryable, Clone)] #[derive(Queryable, Clone)]
pub struct CommentSeers { pub struct CommentSeers {
pub id: i32, pub id: i32,
+29 -30
View File
@@ -1,15 +1,3 @@
use crate::{
comment_seers::{CommentSeers, NewCommentSeers},
instance::Instance,
medias::Media,
mentions::Mention,
notifications::*,
posts::Post,
safe_string::SafeString,
schema::comments,
users::User,
Connection, Error, PlumeRocket, Result,
};
use activitypub::{ use activitypub::{
activity::{Create, Delete}, activity::{Create, Delete},
link, link,
@@ -17,16 +5,26 @@ use activitypub::{
}; };
use chrono::{self, NaiveDateTime}; use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use futures::stream::{self, StreamExt}; use serde_json;
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
},
utils,
};
use std::collections::HashSet; use std::collections::HashSet;
use comment_seers::{CommentSeers, NewCommentSeers};
use instance::Instance;
use medias::Media;
use mentions::Mention;
use notifications::*;
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
};
use plume_common::utils;
use posts::Post;
use safe_string::SafeString;
use schema::comments;
use users::User;
use {Connection, Error, PlumeRocket, Result};
#[derive(Queryable, Identifiable, Clone, AsChangeset)] #[derive(Queryable, Identifiable, Clone, AsChangeset)]
pub struct Comment { pub struct Comment {
pub id: i32, pub id: i32,
@@ -79,7 +77,7 @@ impl Comment {
} }
pub fn count_local(conn: &Connection) -> Result<i64> { pub fn count_local(conn: &Connection) -> Result<i64> {
use crate::schema::users; use schema::users;
let local_authors = users::table let local_authors = users::table
.filter(users::instance_id.eq(Instance::get_local()?.id)) .filter(users::instance_id.eq(Instance::get_local()?.id))
.select(users::id); .select(users::id);
@@ -105,7 +103,7 @@ impl Comment {
.unwrap_or(false) .unwrap_or(false)
} }
pub async fn to_activity(&self, c: &PlumeRocket) -> Result<Note> { pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
let author = User::get(&c.conn, self.author_id)?; let author = User::get(&c.conn, self.author_id)?;
let (html, mentions, _hashtags) = utils::md_to_html( let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(), self.content.get().as_ref(),
@@ -129,21 +127,22 @@ impl Comment {
)?))?; )?))?;
note.object_props note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339())?; .set_published_string(chrono::Utc::now().to_rfc3339())?;
note.object_props.set_attributed_to_link(author.into_id())?; note.object_props
note.object_props.set_to_link_vec(to)?; .set_attributed_to_link(author.clone().into_id())?;
note.object_props.set_to_link_vec(to.clone())?;
note.object_props.set_tag_link_vec( note.object_props.set_tag_link_vec(
stream::iter(mentions) mentions
.filter_map(|m| async move { Mention::build_activity(c, &m).await.ok() }) .into_iter()
.collect::<Vec<link::Mention>>() .filter_map(|m| Mention::build_activity(c, &m).ok())
.await, .collect::<Vec<link::Mention>>(),
)?; )?;
Ok(note) Ok(note)
} }
pub async fn create_activity(&self, c: &PlumeRocket) -> Result<Create> { pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
let author = User::get(&c.conn, self.author_id)?; let author = User::get(&c.conn, self.author_id)?;
let note = self.to_activity(c).await?; let note = self.to_activity(c)?;
let mut act = Create::default(); let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?; act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?; act.create_props.set_object_object(note.clone())?;
-10
View File
@@ -11,8 +11,6 @@ pub struct Config {
pub base_url: String, pub base_url: String,
pub database_url: String, pub database_url: String,
pub db_name: &'static str, pub db_name: &'static str,
pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>,
pub search_index: String, pub search_index: String,
pub rocket: Result<RocketConfig, RocketError>, pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig, pub logo: LogoConfig,
@@ -195,14 +193,6 @@ lazy_static! {
var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned()) var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned())
)), )),
db_name: DB_NAME, db_name: DB_NAME,
db_max_size: var("DB_MAX_SIZE").map_or(None, |s| Some(
s.parse::<u32>()
.expect("Couldn't parse DB_MAX_SIZE into u32")
)),
db_min_idle: var("DB_MIN_IDLE").map_or(None, |s| Some(
s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32")
)),
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
database_url: var("DATABASE_URL") database_url: var("DATABASE_URL")
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)), .unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
+9 -7
View File
@@ -1,4 +1,3 @@
use crate::Connection;
use diesel::r2d2::{ use diesel::r2d2::{
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection, ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
}; };
@@ -7,10 +6,12 @@ use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
use rocket::{ use rocket::{
http::Status, http::Status,
request::{self, FromRequest}, request::{self, FromRequest},
Outcome, Request, Outcome, Request, State,
}; };
use std::ops::Deref; use std::ops::Deref;
use Connection;
pub type DbPool = Pool<ConnectionManager<Connection>>; pub type DbPool = Pool<ConnectionManager<Connection>>;
// From rocket documentation // From rocket documentation
@@ -21,14 +22,14 @@ pub struct DbConn(pub PooledConnection<ConnectionManager<Connection>>);
/// Attempts to retrieve a single connection from the managed database pool. If /// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If /// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status. /// no connections are available, fails with a `ServiceUnavailable` status.
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for DbConn { impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match DbConn::from_request(request).await { let pool = request.guard::<State<DbPool>>()?;
Outcome::Success(a) => Outcome::Success(a), match pool.get() {
_ => Outcome::Failure((Status::ServiceUnavailable, ())), Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
} }
} }
} }
@@ -72,4 +73,5 @@ pub(crate) mod tests {
Ok(conn.begin_test_transaction().unwrap()) Ok(conn.begin_test_transaction().unwrap())
} }
} }
} }
+9 -6
View File
@@ -1,15 +1,16 @@
use crate::{
ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
CONFIG,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo}; use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use notifications::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
broadcast, broadcast,
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, Id, IntoId, PUBLIC_VISIBILITY,
}; };
use schema::follows;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)] #[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
#[belongs_to(User, foreign_key = "following_id")] #[belongs_to(User, foreign_key = "following_id")]
@@ -55,7 +56,8 @@ impl Follow {
let target = User::get(conn, self.following_id)?; let target = User::get(conn, self.following_id)?;
let mut act = FollowAct::default(); let mut act = FollowAct::default();
act.follow_props.set_actor_link::<Id>(user.into_id())?; act.follow_props
.set_actor_link::<Id>(user.clone().into_id())?;
act.follow_props act.follow_props
.set_object_link::<Id>(target.clone().into_id())?; .set_object_link::<Id>(target.clone().into_id())?;
act.object_props.set_id_string(self.ap_url.clone())?; act.object_props.set_id_string(self.ap_url.clone())?;
@@ -200,8 +202,9 @@ impl IntoId for Follow {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{tests::db, users::tests as user_tests};
use diesel::Connection; use diesel::Connection;
use tests::db;
use users::tests as user_tests;
#[test] #[test]
fn test_id() { fn test_id() {
+1 -2
View File
@@ -6,11 +6,10 @@ use rocket::{
pub struct Headers<'r>(pub HeaderMap<'r>); pub struct Headers<'r>(pub HeaderMap<'r>);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
for header in request.headers().clone().into_iter() { for header in request.headers().clone().into_iter() {
headers.add(header); headers.add(header);
+1
View File
@@ -1,4 +1,5 @@
use activitypub::activity::*; use activitypub::activity::*;
use serde_json;
use crate::{ use crate::{
comments::Comment, comments::Comment,
+10 -11
View File
@@ -1,16 +1,15 @@
use crate::{
ap_url,
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{Role, User},
Connection, Error, Result,
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::utils::md_to_html;
use std::sync::RwLock; use std::sync::RwLock;
use ap_url;
use medias::Media;
use plume_common::utils::md_to_html;
use safe_string::SafeString;
use schema::{instances, users};
use users::{Role, User};
use {Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Instance { pub struct Instance {
pub id: i32, pub id: i32,
@@ -57,7 +56,6 @@ impl Instance {
.clone() .clone()
.ok_or(Error::NotFound) .ok_or(Error::NotFound)
} }
pub fn get_local_uncached(conn: &Connection) -> Result<Instance> { pub fn get_local_uncached(conn: &Connection) -> Result<Instance> {
instances::table instances::table
.filter(instances::local.eq(true)) .filter(instances::local.eq(true))
@@ -243,8 +241,9 @@ impl Instance {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{tests::db, Connection as Conn};
use diesel::Connection; use diesel::Connection;
use tests::db;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> { pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
let res = vec![ let res = vec![
+29 -12
View File
@@ -1,22 +1,43 @@
#![feature(try_trait)] #![feature(try_trait)]
#![feature(never_type)] #![feature(never_type)]
#![feature(custom_attribute)]
#![feature(proc_macro_hygiene)] #![feature(proc_macro_hygiene)]
extern crate activitypub;
extern crate ammonia;
extern crate askama_escape;
extern crate bcrypt;
extern crate chrono;
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
extern crate futures; extern crate guid_create;
extern crate heck;
extern crate itertools;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate migrations_internals;
extern crate openssl;
extern crate plume_api;
extern crate plume_common;
#[macro_use] #[macro_use]
extern crate plume_macro; extern crate plume_macro;
extern crate reqwest;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
extern crate rocket_i18n;
extern crate scheduled_thread_pool;
extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[macro_use] #[macro_use]
extern crate tantivy; extern crate tantivy;
extern crate glob;
extern crate url;
extern crate walkdir;
extern crate webfinger;
extern crate whatlang;
use plume_common::activity_pub::inbox::InboxError; use plume_common::activity_pub::inbox::InboxError;
@@ -76,12 +97,6 @@ impl From<std::option::NoneError> for Error {
} }
} }
impl From<Error> for std::option::NoneError {
fn from(_: Error) -> Self {
std::option::NoneError
}
}
impl From<url::ParseError> for Error { impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self { fn from(_: url::ParseError) -> Self {
Error::Url Error::Url
@@ -235,13 +250,12 @@ macro_rules! get {
/// Model::insert(connection, NewModelType::new()); /// Model::insert(connection, NewModelType::new());
/// ``` /// ```
macro_rules! insert { macro_rules! insert {
($table:ident, $from:ty) => { ($table:ident, $from:ident) => {
insert!($table, $from, |x, _conn| Ok(x)); insert!($table, $from, |x, _conn| Ok(x));
}; };
($table:ident, $from:ty, |$val:ident, $conn:ident | $( $after:tt )+) => { ($table:ident, $from:ident, |$val:ident, $conn:ident | $( $after:tt )+) => {
last!($table); last!($table);
#[allow(dead_code)]
pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> { pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
diesel::insert_into($table::table) diesel::insert_into($table::table)
.values(new) .values(new)
@@ -268,7 +282,6 @@ macro_rules! insert {
/// ``` /// ```
macro_rules! last { macro_rules! last {
($table:ident) => { ($table:ident) => {
#[allow(dead_code)]
pub fn last(conn: &crate::Connection) -> Result<Self> { pub fn last(conn: &crate::Connection) -> Result<Self> {
$table::table $table::table
.order_by($table::id.desc()) .order_by($table::id.desc())
@@ -288,12 +301,16 @@ pub fn ap_url(url: &str) -> String {
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
mod tests { mod tests {
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, search, Connection as Conn, CONFIG}; use db_conn;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use migrations::IMPORTED_MIGRATIONS;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use search;
use std::env::temp_dir; use std::env::temp_dir;
use std::sync::Arc; use std::sync::Arc;
use Connection as Conn;
use CONFIG;
#[macro_export] #[macro_export]
macro_rules! part_eq { macro_rules! part_eq {
+7 -4
View File
@@ -1,14 +1,17 @@
use crate::{
notifications::*, posts::Post, schema::likes, timeline::*, users::User, Connection, Error,
PlumeRocket, Result,
};
use activitypub::activity; use activitypub::activity;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use notifications::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY, Id, IntoId, PUBLIC_VISIBILITY,
}; };
use posts::Post;
use schema::likes;
use timeline::*;
use users::User;
use {Connection, Error, PlumeRocket, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct Like { pub struct Like {
+31 -70
View File
@@ -1,14 +1,13 @@
use crate::{
blogs::Blog,
schema::{blogs, list_elems, lists, users},
users::User,
Connection, Error, Result,
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::convert::{TryFrom, TryInto};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] use blogs::Blog;
use schema::{blogs, list_elems, lists, users};
use std::convert::{TryFrom, TryInto};
use users::User;
use {Connection, Error, Result};
/// Represent what a list is supposed to store. Represented in database as an integer /// Represent what a list is supposed to store. Represented in database as an integer
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ListType { pub enum ListType {
User, User,
Blog, Blog,
@@ -58,11 +57,7 @@ struct NewList<'a> {
} }
macro_rules! func { macro_rules! func {
( (@elem User $id:expr, $value:expr) => {
$(#[$outer:meta])*
@elem User $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: Some(*$value), user_id: Some(*$value),
@@ -70,11 +65,7 @@ macro_rules! func {
word: None, word: None,
} }
}; };
( (@elem Blog $id:expr, $value:expr) => {
$(#[$outer:meta])*
@elem Blog $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: None, user_id: None,
@@ -82,11 +73,7 @@ macro_rules! func {
word: None, word: None,
} }
}; };
( (@elem Word $id:expr, $value:expr) => {
$(#[$outer:meta])*
@elem Word $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: None, user_id: None,
@@ -94,11 +81,7 @@ macro_rules! func {
word: Some($value), word: Some($value),
} }
}; };
( (@elem Prefix $id:expr, $value:expr) => {
$(#[$outer:meta])*
@elem Prefix $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: None, user_id: None,
@@ -115,11 +98,7 @@ macro_rules! func {
(@out_type Word) => { String }; (@out_type Word) => { String };
(@out_type Prefix) => { String }; (@out_type Prefix) => { String };
( (add: $fn:ident, $kind:ident) => {
$(#[$outer:meta])*
add: $fn:ident, $kind:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> { pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> {
if self.kind() != ListType::$kind { if self.kind() != ListType::$kind {
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
@@ -136,11 +115,7 @@ macro_rules! func {
} }
}; };
( (list: $fn:ident, $kind:ident, $table:ident) => {
$(#[$outer:meta])*
list: $fn:ident, $kind:ident, $table:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> { pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> {
if self.kind() != ListType::$kind { if self.kind() != ListType::$kind {
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
@@ -156,11 +131,7 @@ macro_rules! func {
( (set: $fn:ident, $kind:ident, $add:ident) => {
$(#[$outer:meta])*
set: $fn:ident, $kind:ident, $add:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> { pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> {
if self.kind() != ListType::$kind { if self.kind() != ListType::$kind {
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
@@ -193,7 +164,7 @@ impl List {
last!(lists); last!(lists);
get!(lists); get!(lists);
fn insert(conn: &Connection, val: NewList<'_>) -> Result<Self> { fn insert(conn: &Connection, val: NewList) -> Result<Self> {
diesel::insert_into(lists::table) diesel::insert_into(lists::table)
.values(val) .values(val)
.execute(conn)?; .execute(conn)?;
@@ -274,35 +245,23 @@ impl List {
private::ListElem::prefix_in_list(conn, self, word) private::ListElem::prefix_in_list(conn, self, word)
} }
func! { /// Insert new users in a list
/// Insert new users in a list func! {add: add_users, User}
add: add_users, User
}
func! { /// Insert new blogs in a list
/// Insert new blogs in a list func! {add: add_blogs, Blog}
add: add_blogs, Blog
}
func! { /// Insert new words in a list
/// Insert new words in a list func! {add: add_words, Word}
add: add_words, Word
}
func! { /// Insert new prefixes in a list
/// Insert new prefixes in a list func! {add: add_prefixes, Prefix}
add: add_prefixes, Prefix
}
func! { /// Get all users in the list
/// Get all users in the list func! {list: list_users, User, users}
list: list_users, User, users
}
func! { /// Get all blogs in the list
/// Get all blogs in the list func! {list: list_blogs, Blog, blogs}
list: list_blogs, Blog, blogs
}
/// Get all words in the list /// Get all words in the list
pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> { pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> {
@@ -350,7 +309,7 @@ mod private {
}; };
impl ListElem { impl ListElem {
insert!(list_elems, NewListElem<'_>); insert!(list_elems, NewListElem);
pub fn user_in_list(conn: &Connection, list: &List, user: i32) -> Result<bool> { pub fn user_in_list(conn: &Connection, list: &List, user: i32) -> Result<bool> {
dsl::select(dsl::exists( dsl::select(dsl::exists(
@@ -400,8 +359,9 @@ mod private {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{blogs::tests as blog_tests, tests::db}; use blogs::tests as blog_tests;
use diesel::Connection; use diesel::Connection;
use tests::db;
#[test] #[test]
fn list_type() { fn list_type() {
@@ -591,4 +551,5 @@ mod tests {
Ok(()) Ok(())
}); });
} }
} }
+21 -18
View File
@@ -1,17 +1,20 @@
use crate::{
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
Error, PlumeRocket, Result,
};
use activitypub::object::Image; use activitypub::object::Image;
use askama_escape::escape; use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID; use guid_create::GUID;
use reqwest;
use std::{fs, path::Path};
use plume_common::{ use plume_common::{
activity_pub::{inbox::FromId, Id}, activity_pub::{inbox::FromId, Id},
utils::MediaProcessor, utils::MediaProcessor,
}; };
use std::{fs, path::Path};
use tokio::prelude::*; use instance::Instance;
use safe_string::SafeString;
use schema::medias;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result};
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Media { pub struct Media {
@@ -114,19 +117,15 @@ impl Media {
Ok(match self.category() { Ok(match self.category() {
MediaCategory::Image => SafeString::trusted(&format!( MediaCategory::Image => SafeString::trusted(&format!(
r#"<img src="{}" alt="{}" title="{}">"#, r#"<img src="{}" alt="{}" title="{}">"#,
url, url, escape(&self.alt_text), escape(&self.alt_text)
escape(&self.alt_text),
escape(&self.alt_text)
)), )),
MediaCategory::Audio => SafeString::trusted(&format!( MediaCategory::Audio => SafeString::trusted(&format!(
r#"<div class="media-preview audio"></div><audio src="{}" title="{}" controls></audio>"#, r#"<div class="media-preview audio"></div><audio src="{}" title="{}" controls></audio>"#,
url, url, escape(&self.alt_text)
escape(&self.alt_text)
)), )),
MediaCategory::Video => SafeString::trusted(&format!( MediaCategory::Video => SafeString::trusted(&format!(
r#"<video src="{}" title="{}" controls></video>"#, r#"<video src="{}" title="{}" controls></video>"#,
url, url, escape(&self.alt_text)
escape(&self.alt_text)
)), )),
MediaCategory::Unknown => SafeString::trusted(&format!( MediaCategory::Unknown => SafeString::trusted(&format!(
r#"<a href="{}" class="media-preview unknown"></a>"#, r#"<a href="{}" class="media-preview unknown"></a>"#,
@@ -197,7 +196,7 @@ impl Media {
} }
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub async fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> { pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
let conn = &*c.conn; let conn = &*c.conn;
let remote_url = image.object_props.url_string().ok()?; let remote_url = image.object_props.url_string().ok()?;
let ext = remote_url let ext = remote_url
@@ -211,9 +210,11 @@ impl Media {
ext ext
)); ));
let mut dest = tokio::fs::File::create(path.clone()).await?; let mut dest = fs::File::create(path.clone()).ok()?;
let contents = reqwest::get(remote_url.as_str()).await?.bytes().await?; reqwest::get(remote_url.as_str())
dest.write_all(&contents).await?; .ok()?
.copy_to(&mut dest)
.ok()?;
Media::insert( Media::insert(
conn, conn,
@@ -258,11 +259,13 @@ impl Media {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{tests::db, users::tests as usersTests, Connection as Conn};
use diesel::Connection; use diesel::Connection;
use std::env::{current_dir, set_current_dir}; use std::env::{current_dir, set_current_dir};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use tests::db;
use users::tests as usersTests;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) { pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
let mut wd = current_dir().unwrap().to_path_buf(); let mut wd = current_dir().unwrap().to_path_buf();
+10 -6
View File
@@ -1,10 +1,14 @@
use crate::{
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
Error, PlumeRocket, Result,
};
use activitypub::link; use activitypub::link;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use comments::Comment;
use notifications::*;
use plume_common::activity_pub::inbox::AsActor; use plume_common::activity_pub::inbox::AsActor;
use posts::Post;
use schema::mentions;
use users::User;
use PlumeRocket;
use {Connection, Error, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct Mention { pub struct Mention {
@@ -52,8 +56,8 @@ impl Mention {
} }
} }
pub async fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> { pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(c, ment).await?; let user = User::find_by_fqn(c, ment)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url)?; mention.link_props.set_href_string(user.ap_url)?;
mention.link_props.set_name_string(format!("@{}", ment))?; mention.link_props.set_name_string(format!("@{}", ment))?;
+6 -2
View File
@@ -1,12 +1,16 @@
use crate::{Connection, Error, Result}; use Connection;
use Error;
use Result;
use diesel::connection::{Connection as Conn, SimpleConnection}; use diesel::connection::{Connection as Conn, SimpleConnection};
use migrations_internals::{setup_database, MigrationConnection}; use migrations_internals::{setup_database, MigrationConnection};
use std::path::Path; use std::path::Path;
#[allow(dead_code)] //variants might not be constructed if not required by current migrations #[allow(dead_code)] //variants might not be constructed if not required by current migrations
enum Action { enum Action {
Sql(&'static str), Sql(&'static str),
Function(&'static dyn Fn(&Connection, &Path) -> Result<()>), Function(&'static Fn(&Connection, &Path) -> Result<()>),
} }
impl Action { impl Action {
+10 -11
View File
@@ -1,17 +1,16 @@
use crate::{
comments::Comment,
follows::Follow,
likes::Like,
mentions::Mention,
posts::Post,
reshares::Reshare,
schema::{follows, notifications},
users::User,
Connection, Error, Result,
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl};
use comments::Comment;
use follows::Follow;
use likes::Like;
use mentions::Mention;
use posts::Post;
use reshares::Reshare;
use schema::{follows, notifications};
use users::User;
use {Connection, Error, Result};
pub mod notification_kind { pub mod notification_kind {
pub const COMMENT: &str = "COMMENT"; pub const COMMENT: &str = "COMMENT";
pub const FOLLOW: &str = "FOLLOW"; pub const FOLLOW: &str = "FOLLOW";
+4 -2
View File
@@ -1,6 +1,7 @@
use crate::{schema::password_reset_requests, Connection, Error, Result};
use chrono::{offset::Utc, Duration, NaiveDateTime}; use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::password_reset_requests;
use {Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct PasswordResetRequest { pub struct PasswordResetRequest {
@@ -74,8 +75,9 @@ impl PasswordResetRequest {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{tests::db, users::tests as user_tests};
use diesel::Connection; use diesel::Connection;
use tests::db;
use users::tests as user_tests;
#[test] #[test]
fn test_insert_and_find_password_reset_request() { fn test_insert_and_find_password_reset_request() {
+20 -32
View File
@@ -2,7 +2,9 @@ pub use self::module::PlumeRocket;
#[cfg(not(test))] #[cfg(not(test))]
mod module { mod module {
use crate::{db_conn::DbConn, search, users}; use crate::db_conn::DbConn;
use crate::search;
use crate::users;
use rocket::{ use rocket::{
request::{self, FlashMessage, FromRequest, Request}, request::{self, FlashMessage, FromRequest, Request},
Outcome, State, Outcome, State,
@@ -20,35 +22,20 @@ mod module {
pub flash_msg: Option<(String, String)>, pub flash_msg: Option<(String, String)>,
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket { impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = DbConn::from_request(request).await.succeeded().unwrap(); let conn = request.guard::<DbConn>()?;
let intl = rocket_i18n::I18n::from_request(request) let intl = request.guard::<rocket_i18n::I18n>()?;
.await let user = request.guard::<users::User>().succeeded();
.succeeded() let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
.unwrap(); let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
let user = users::User::from_request(request) let flash_msg = request.guard::<FlashMessage>().succeeded();
.await
.succeeded()
.unwrap();
let worker = request
.guard::<State<'_, Arc<ScheduledThreadPool>>>()
.await
.succeeded()
.unwrap();
let searcher = request
.guard::<State<'_, Arc<search::Searcher>>>()
.await
.succeeded()
.unwrap();
let flash_msg = request.guard::<FlashMessage<'_, '_>>().await.succeeded();
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn, conn,
intl, intl,
user: Some(user), user,
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())), flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
worker: worker.clone(), worker: worker.clone(),
searcher: searcher.clone(), searcher: searcher.clone(),
@@ -59,7 +46,9 @@ mod module {
#[cfg(test)] #[cfg(test)]
mod module { mod module {
use crate::{db_conn::DbConn, search, users}; use crate::db_conn::DbConn;
use crate::search;
use crate::users;
use rocket::{ use rocket::{
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
Outcome, State, Outcome, State,
@@ -75,18 +64,17 @@ mod module {
pub worker: Arc<ScheduledThreadPool>, pub worker: Arc<ScheduledThreadPool>,
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket { impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = try_outcome!(DbConn::from_request(request).await); let conn = request.guard::<DbConn>()?;
let user = try_outcome!(users::User::from_request(request).await); let user = request.guard::<users::User>().succeeded();
let worker = try_outcome!(request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()); let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
let searcher = try_outcome!(request.guard::<'_, State<'_, Arc<search::Searcher>>>()); let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn, conn,
user: Some(user), user,
worker: worker.clone(), worker: worker.clone(),
searcher: searcher.clone(), searcher: searcher.clone(),
}) })
+5 -1
View File
@@ -1,6 +1,10 @@
use crate::{posts::Post, schema::post_authors, users::User, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use posts::Post;
use schema::post_authors;
use users::User;
use {Error, Result};
#[derive(Clone, Queryable, Identifiable, Associations)] #[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(Post)] #[belongs_to(Post)]
#[belongs_to(User, foreign_key = "author_id")] #[belongs_to(User, foreign_key = "author_id")]
+41 -38
View File
@@ -1,8 +1,3 @@
use crate::{
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
safe_string::SafeString, schema::posts, search::Searcher, tags::*, timeline::*, users::User,
Connection, Error, PlumeRocket, Result, CONFIG,
};
use activitypub::{ use activitypub::{
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
link, link,
@@ -12,6 +7,13 @@ use activitypub::{
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use serde_json;
use std::collections::HashSet;
use blogs::Blog;
use instance::Instance;
use medias::Media;
use mentions::Mention;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsObject, FromId}, inbox::{AsObject, FromId},
@@ -19,8 +21,14 @@ use plume_common::{
}, },
utils::md_to_html, utils::md_to_html,
}; };
use std::collections::HashSet; use post_authors::*;
use tokio::runtime::Runtime; use safe_string::SafeString;
use schema::posts;
use search::Searcher;
use tags::*;
use timeline::*;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
pub type LicensedArticle = CustomObject<Licensed, Article>; pub type LicensedArticle = CustomObject<Licensed, Article>;
@@ -103,7 +111,7 @@ impl Post {
tag: String, tag: String,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
use crate::schema::tags; use schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table posts::table
@@ -117,7 +125,7 @@ impl Post {
} }
pub fn count_for_tag(conn: &Connection, tag: String) -> Result<i64> { pub fn count_for_tag(conn: &Connection, tag: String) -> Result<i64> {
use crate::schema::tags; use schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table posts::table
.filter(posts::id.eq_any(ids)) .filter(posts::id.eq_any(ids))
@@ -131,8 +139,8 @@ impl Post {
} }
pub fn count_local(conn: &Connection) -> Result<i64> { pub fn count_local(conn: &Connection) -> Result<i64> {
use crate::schema::post_authors; use schema::post_authors;
use crate::schema::users; use schema::users;
let local_authors = users::table let local_authors = users::table
.filter(users::instance_id.eq(Instance::get_local()?.id)) .filter(users::instance_id.eq(Instance::get_local()?.id))
.select(users::id); .select(users::id);
@@ -180,7 +188,7 @@ impl Post {
author: &User, author: &User,
limit: i64, limit: i64,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
use crate::schema::post_authors; use schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table posts::table
@@ -231,7 +239,7 @@ impl Post {
} }
pub fn drafts_by_author(conn: &Connection, author: &User) -> Result<Vec<Post>> { pub fn drafts_by_author(conn: &Connection, author: &User) -> Result<Vec<Post>> {
use crate::schema::post_authors; use schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table posts::table
@@ -243,8 +251,8 @@ impl Post {
} }
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> { pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::post_authors; use schema::post_authors;
use crate::schema::users; use schema::users;
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id); let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
users::table users::table
.filter(users::id.eq_any(author_list)) .filter(users::id.eq_any(author_list))
@@ -253,7 +261,7 @@ impl Post {
} }
pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result<bool> { pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result<bool> {
use crate::schema::post_authors; use schema::post_authors;
Ok(PostAuthor::belonging_to(self) Ok(PostAuthor::belonging_to(self)
.filter(post_authors::author_id.eq(author_id)) .filter(post_authors::author_id.eq(author_id))
.count() .count()
@@ -262,7 +270,7 @@ impl Post {
} }
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> { pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
use crate::schema::blogs; use schema::blogs;
blogs::table blogs::table
.filter(blogs::id.eq(self.blog_id)) .filter(blogs::id.eq(self.blog_id))
.first(conn) .first(conn)
@@ -270,7 +278,7 @@ impl Post {
} }
pub fn count_likes(&self, conn: &Connection) -> Result<i64> { pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
use crate::schema::likes; use schema::likes;
likes::table likes::table
.filter(likes::post_id.eq(self.id)) .filter(likes::post_id.eq(self.id))
.count() .count()
@@ -279,7 +287,7 @@ impl Post {
} }
pub fn count_reshares(&self, conn: &Connection) -> Result<i64> { pub fn count_reshares(&self, conn: &Connection) -> Result<i64> {
use crate::schema::reshares; use schema::reshares;
reshares::table reshares::table
.filter(reshares::post_id.eq(self.id)) .filter(reshares::post_id.eq(self.id))
.count() .count()
@@ -579,11 +587,11 @@ impl FromId<PlumeRocket> for Post {
} }
}); });
let image = article.object_props.icon_object::<Image>().ok().unwrap(); let cover = article
.object_props
let mut r = Runtime::new().unwrap(); .icon_object::<Image>()
let cover = .ok()
Some(r.block_on(async { Media::from_activity(&c, &image).await.ok().unwrap().id })); .and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?; let title = article.object_props.name_string()?;
let post = Post::insert( let post = Post::insert(
@@ -624,7 +632,7 @@ impl FromId<PlumeRocket> for Post {
.into_iter() .into_iter()
.map(|s| s.to_camel_case()) .map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
for tag in tags { for tag in tags {
serde_json::from_value::<link::Mention>(tag.clone()) serde_json::from_value::<link::Mention>(tag.clone())
.map(|m| Mention::from_activity(conn, &m, post.id, true, true)) .map(|m| Mention::from_activity(conn, &m, post.id, true, true))
@@ -699,22 +707,17 @@ impl FromId<PlumeRocket> for PostUpdate {
} }
fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> { fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> {
let image = updated
.object
.object_props
.icon_object::<Image>()
.ok()
.unwrap();
let mut r = Runtime::new().unwrap();
let cover =
Some(r.block_on(async { Media::from_activity(&c, &image).await.ok().unwrap().id }));
Ok(PostUpdate { Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?, ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(), title: updated.object.object_props.name_string().ok(),
subtitle: updated.object.object_props.summary_string().ok(), subtitle: updated.object.object_props.summary_string().ok(),
content: updated.object.object_props.content_string().ok(), content: updated.object.object_props.content_string().ok(),
cover, cover: updated
.object
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)),
source: updated source: updated
.object .object
.ap_object_props .ap_object_props
@@ -722,7 +725,7 @@ impl FromId<PlumeRocket> for PostUpdate {
.ok() .ok()
.map(|x| x.content), .map(|x| x.content),
license: updated.custom_props.license_string().ok(), license: updated.custom_props.license_string().ok(),
tags: updated.object.object_props.tag, tags: updated.object.object_props.tag.clone(),
}) })
} }
} }
@@ -803,7 +806,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
impl IntoId for Post { impl IntoId for Post {
fn into_id(self) -> Id { fn into_id(self) -> Id {
Id::new(self.ap_url) Id::new(self.ap_url.clone())
} }
} }
+7 -4
View File
@@ -1,14 +1,17 @@
use crate::{
notifications::*, posts::Post, schema::reshares, timeline::*, users::User, Connection, Error,
PlumeRocket, Result,
};
use activitypub::activity::{Announce, Undo}; use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use notifications::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY, Id, IntoId, PUBLIC_VISIBILITY,
}; };
use posts::Post;
use schema::reshares;
use timeline::*;
use users::User;
use {Connection, Error, PlumeRocket, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct Reshare { pub struct Reshare {
+4 -4
View File
@@ -82,7 +82,7 @@ lazy_static! {
}; };
} }
fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> { fn url_add_prefix(url: &str) -> Option<Cow<str>> {
if url.starts_with('#') && !url.starts_with("#postcontent-") { if url.starts_with('#') && !url.starts_with("#postcontent-") {
//if start with an # //if start with an #
let mut new_url = "#postcontent-".to_owned(); //change to valid id let mut new_url = "#postcontent-".to_owned(); //change to valid id
@@ -139,7 +139,7 @@ struct SafeStringVisitor;
impl<'de> Visitor<'de> for SafeStringVisitor { impl<'de> Visitor<'de> for SafeStringVisitor {
type Value = SafeString; type Value = SafeString;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string") formatter.write_str("a string")
} }
@@ -181,7 +181,7 @@ where
DB: diesel::backend::Backend, DB: diesel::backend::Backend,
str: ToSql<diesel::sql_types::Text, DB>, str: ToSql<diesel::sql_types::Text, DB>,
{ {
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, DB>) -> serialize::Result { fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
str::to_sql(&self.value, out) str::to_sql(&self.value, out)
} }
} }
@@ -193,7 +193,7 @@ impl Borrow<str> for SafeString {
} }
impl Display for SafeString { impl Display for SafeString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.value) write!(f, "{}", self.value)
} }
} }
-1
View File
@@ -29,7 +29,6 @@ table! {
is_owner -> Bool, is_owner -> Bool,
} }
} }
table! { table! {
blogs (id) { blogs (id) {
id -> Int4, id -> Int4,
+6 -8
View File
@@ -8,17 +8,15 @@ pub use self::searcher::*;
pub(crate) mod tests { pub(crate) mod tests {
use super::{Query, Searcher}; use super::{Query, Searcher};
use diesel::Connection; use diesel::Connection;
use plume_common::utils::random_hex;
use std::env::temp_dir; use std::env::temp_dir;
use std::str::FromStr; use std::str::FromStr;
use crate::{ use blogs::tests::fill_database;
blogs::tests::fill_database, use plume_common::utils::random_hex;
post_authors::*, use post_authors::*;
posts::{NewPost, Post}, use posts::{NewPost, Post};
safe_string::SafeString, use safe_string::SafeString;
tests::db, use tests::db;
};
pub(crate) fn get_searcher() -> Searcher { pub(crate) fn get_searcher() -> Searcher {
let dir = temp_dir().join(&format!("plume-test-{}", random_hex())); let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
+3 -3
View File
@@ -1,5 +1,5 @@
use crate::search::searcher::Searcher;
use chrono::{naive::NaiveDate, offset::Utc, Datelike}; use chrono::{naive::NaiveDate, offset::Utc, Datelike};
use search::searcher::Searcher;
use std::{cmp, ops::Bound}; use std::{cmp, ops::Bound};
use tantivy::{query::*, schema::*, Term}; use tantivy::{query::*, schema::*, Term};
@@ -153,7 +153,7 @@ impl PlumeQuery {
/// Convert this Query to a Tantivy Query /// Convert this Query to a Tantivy Query
pub fn into_query(self) -> BooleanQuery { pub fn into_query(self) -> BooleanQuery {
let mut result: Vec<(Occur, Box<dyn Query>)> = Vec::new(); let mut result: Vec<(Occur, Box<Query>)> = Vec::new();
gen_to_query!(self, result; normal: title, subtitle, content, tag; gen_to_query!(self, result; normal: title, subtitle, content, tag;
oneoff: instance, author, blog, lang, license); oneoff: instance, author, blog, lang, license);
@@ -279,7 +279,7 @@ impl PlumeQuery {
} }
// map a token and it's field to a query // map a token and it's field to a query
fn token_to_query(token: &str, field_name: &str) -> Box<dyn Query> { fn token_to_query(token: &str, field_name: &str) -> Box<Query> {
let token = token.to_lowercase(); let token = token.to_lowercase();
let token = token.as_str(); let token = token.as_str();
let field = Searcher::schema().get_field(field_name).unwrap(); let field = Searcher::schema().get_field(field_name).unwrap();
+13 -11
View File
@@ -1,11 +1,9 @@
use crate::{ use instance::Instance;
instance::Instance, use posts::Post;
posts::Post, use schema::posts;
schema::posts, use tags::Tag;
search::{query::PlumeQuery, tokenizer}, use Connection;
tags::Tag,
Connection, Result,
};
use chrono::Datelike; use chrono::Datelike;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use itertools::Itertools; use itertools::Itertools;
@@ -16,6 +14,10 @@ use tantivy::{
}; };
use whatlang::{detect as detect_lang, Lang}; use whatlang::{detect as detect_lang, Lang};
use super::tokenizer;
use search::query::PlumeQuery;
use Result;
#[derive(Debug)] #[derive(Debug)]
pub enum SearcherError { pub enum SearcherError {
IndexCreationError, IndexCreationError,
@@ -70,7 +72,7 @@ impl Searcher {
schema_builder.build() schema_builder.build()
} }
pub fn create(path: &dyn AsRef<Path>) -> Result<Self> { pub fn create(path: &AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser); let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer let content_tokenizer = SimpleTokenizer
@@ -109,7 +111,7 @@ impl Searcher {
}) })
} }
pub fn open(path: &dyn AsRef<Path>) -> Result<Self> { pub fn open(path: &AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser); let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer let content_tokenizer = SimpleTokenizer
@@ -173,7 +175,7 @@ impl Searcher {
post_id => i64::from(post.id), post_id => i64::from(post.id),
author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "), author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "),
creation_date => i64::from(post.creation_date.num_days_from_ce()), creation_date => i64::from(post.creation_date.num_days_from_ce()),
instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain, instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain.clone(),
tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "), tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),
blog_name => post.get_blog(conn)?.title, blog_name => post.get_blog(conn)?.title,
content => post.content.get().clone(), content => post.content.get().clone(),
+4 -1
View File
@@ -1,6 +1,9 @@
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use instance::Instance;
use plume_common::activity_pub::Hashtag; use plume_common::activity_pub::Hashtag;
use schema::tags;
use {ap_url, Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Tag { pub struct Tag {
+22 -23
View File
@@ -1,17 +1,17 @@
use crate::{
lists::List,
posts::Post,
schema::{posts, timeline, timeline_definition},
Connection, Error, PlumeRocket, Result,
};
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
use lists::List;
use posts::Post;
use schema::{posts, timeline, timeline_definition};
use std::ops::Deref; use std::ops::Deref;
use {Connection, Error, PlumeRocket, Result};
pub(crate) mod query; pub(crate) mod query;
pub use self::query::Kind;
use self::query::{QueryError, TimelineQuery}; use self::query::{QueryError, TimelineQuery};
pub use self::query::Kind;
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)] #[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
#[table_name = "timeline_definition"] #[table_name = "timeline_definition"]
pub struct Timeline { pub struct Timeline {
@@ -119,7 +119,7 @@ impl Timeline {
} }
}) })
{ {
return Err(err); Err(err)?;
} }
} }
Self::insert( Self::insert(
@@ -157,7 +157,7 @@ impl Timeline {
} }
}) })
{ {
return Err(err); Err(err)?;
} }
} }
Self::insert( Self::insert(
@@ -208,7 +208,7 @@ impl Timeline {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> { pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind) -> Result<()> {
let timelines = timeline_definition::table let timelines = timeline_definition::table
.load::<Self>(rocket.conn.deref()) .load::<Self>(rocket.conn.deref())
.map_err(Error::from)?; .map_err(Error::from)?;
@@ -231,7 +231,7 @@ impl Timeline {
Ok(()) Ok(())
} }
pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> { pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?; let query = TimelineQuery::parse(&self.query)?;
query.matches(rocket, self, post, kind) query.matches(rocket, self, post, kind)
} }
@@ -240,18 +240,16 @@ impl Timeline {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use blogs::tests as blogTests;
blogs::tests as blogTests,
follows::*,
lists::ListType,
post_authors::{NewPostAuthor, PostAuthor},
posts::NewPost,
safe_string::SafeString,
tags::Tag,
tests::{db, rockets},
users::tests as userTests,
};
use diesel::Connection; use diesel::Connection;
use follows::*;
use lists::ListType;
use post_authors::{NewPostAuthor, PostAuthor};
use posts::NewPost;
use safe_string::SafeString;
use tags::Tag;
use tests::{db, rockets};
use users::tests as userTests;
#[test] #[test]
fn test_timeline() { fn test_timeline() {
@@ -497,7 +495,8 @@ mod tests {
), ),
published: true, published: true,
license: "GPL".to_string(), license: "GPL".to_string(),
source: "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better.".to_string(), source: "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better."
.to_string(),
ap_url: "".to_string(), ap_url: "".to_string(),
creation_date: None, creation_date: None,
subtitle: "".to_string(), subtitle: "".to_string(),
+30 -59
View File
@@ -1,17 +1,15 @@
use crate::{ use blogs::Blog;
blogs::Blog, use lists::{self, ListType};
lists::{self, ListType},
posts::Post,
tags::Tag,
timeline::Timeline,
users::User,
PlumeRocket, Result,
};
use futures::stream::{self, StreamExt};
use plume_common::activity_pub::inbox::AsActor; use plume_common::activity_pub::inbox::AsActor;
use tokio::runtime::Runtime; use posts::Post;
use tags::Tag;
use users::User;
use whatlang::{self, Lang}; use whatlang::{self, Lang};
use {PlumeRocket, Result};
use super::Timeline;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum QueryError { pub enum QueryError {
SyntaxError(usize, usize, String), SyntaxError(usize, usize, String),
@@ -67,7 +65,7 @@ impl<'a> Token<'a> {
} }
} }
fn get_error<T>(&self, token: Token<'_>) -> QueryResult<T> { fn get_error<T>(&self, token: Token) -> QueryResult<T> {
let (b, e) = self.get_pos(); let (b, e) = self.get_pos();
let message = format!( let message = format!(
"Syntax Error: Expected {}, got {}", "Syntax Error: Expected {}, got {}",
@@ -81,7 +79,7 @@ impl<'a> Token<'a> {
impl<'a> ToString for Token<'a> { impl<'a> ToString for Token<'a> {
fn to_string(&self) -> String { fn to_string(&self) -> String {
if let Token::Word(0, 0, v) = self { if let Token::Word(0, 0, v) = self {
return (*v).to_string(); return v.to_string();
} }
format!( format!(
"'{}'", "'{}'",
@@ -129,7 +127,7 @@ macro_rules! gen_tokenizer {
} }
} }
fn lex(stream: &str) -> Vec<Token<'_>> { fn lex(stream: &str) -> Vec<Token> {
stream stream
.chars() .chars()
.chain(" ".chars()) // force a last whitespace to empty scan's state .chain(" ".chars()) // force a last whitespace to empty scan's state
@@ -165,7 +163,7 @@ impl<'a> TQ<'a> {
rocket: &PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind,
) -> Result<bool> { ) -> Result<bool> {
match self { match self {
TQ::Or(inner) => inner.iter().try_fold(false, |s, e| { TQ::Or(inner) => inner.iter().try_fold(false, |s, e| {
@@ -183,7 +181,7 @@ impl<'a> TQ<'a> {
TQ::Or(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(), TQ::Or(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(),
TQ::And(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(), TQ::And(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(),
TQ::Arg(Arg::In(typ, List::List(name)), _) => vec![( TQ::Arg(Arg::In(typ, List::List(name)), _) => vec![(
(*name).to_string(), name.to_string(),
match typ { match typ {
WithList::Blog => ListType::Blog, WithList::Blog => ListType::Blog,
WithList::Author { .. } => ListType::User, WithList::Author { .. } => ListType::User,
@@ -210,7 +208,7 @@ impl<'a> Arg<'a> {
rocket: &PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind,
) -> Result<bool> { ) -> Result<bool> {
match self { match self {
Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind), Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind),
@@ -235,8 +233,8 @@ impl WithList {
rocket: &PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
list: &List<'_>, list: &List,
kind: Kind<'_>, kind: Kind,
) -> Result<bool> { ) -> Result<bool> {
match list { match list {
List::List(name) => { List::List(name) => {
@@ -292,38 +290,19 @@ impl WithList {
(_, _) => Err(QueryError::RuntimeError(format!( (_, _) => Err(QueryError::RuntimeError(format!(
"The list '{}' is of the wrong type for this usage", "The list '{}' is of the wrong type for this usage",
name name
)) )))?,
.into()),
} }
} }
List::Array(list) => match self { List::Array(list) => match self {
WithList::Blog => { WithList::Blog => Ok(list
let mut rt = Runtime::new().unwrap(); .iter()
rt.block_on(async move { .filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
Ok(stream::iter(list) .any(|b| b.id == post.blog_id)),
.filter_map(|b| async move {
Some(Blog::find_by_fqn(rocket, b).await.ok().unwrap())
})
.collect::<Vec<_>>()
.await
.into_iter()
.any(|b| b.id == post.blog_id))
})
}
WithList::Author { boosts, likes } => match kind { WithList::Author { boosts, likes } => match kind {
Kind::Original => { Kind::Original => Ok(list
let mut rt = Runtime::new().unwrap(); .iter()
rt.block_on(async move { .filter_map(|a| User::find_by_fqn(rocket, a).ok())
Ok(stream::iter(list) .any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
.filter_map(|a| async move {
Some(User::find_by_fqn(rocket, a).await.ok().unwrap())
})
.collect::<Vec<_>>()
.await
.into_iter()
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false)))
})
}
Kind::Reshare(u) => { Kind::Reshare(u) => {
if *boosts { if *boosts {
Ok(list.iter().any(|user| &u.fqn == user)) Ok(list.iter().any(|user| &u.fqn == user))
@@ -394,7 +373,7 @@ impl Bool {
rocket: &PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind,
) -> Result<bool> { ) -> Result<bool> {
match self { match self {
Bool::Followed { boosts, likes } => { Bool::Followed { boosts, likes } => {
@@ -665,7 +644,7 @@ impl<'a> TimelineQuery<'a> {
rocket: &PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind,
) -> Result<bool> { ) -> Result<bool> {
self.0.matches(rocket, timeline, post, kind) self.0.matches(rocket, timeline, post, kind)
} }
@@ -832,17 +811,9 @@ mod tests {
); );
let expect_keyword = TimelineQuery::parse(r#"not_a_field contains something"#).unwrap_err(); let expect_keyword = TimelineQuery::parse(r#"not_a_field contains something"#).unwrap_err();
assert_eq!( assert_eq!(expect_keyword, QueryError::SyntaxError(0, 11, "Syntax Error: Expected one of 'blog', \
expect_keyword,
QueryError::SyntaxError(
0,
11,
"Syntax Error: Expected one of 'blog', \
'author', 'license', 'tags', 'lang', 'title', 'subtitle', 'content', 'followed', 'has_cover', \ 'author', 'license', 'tags', 'lang', 'title', 'subtitle', 'content', 'followed', 'has_cover', \
'local' or 'all', got 'not_a_field'" 'local' or 'all', got 'not_a_field'".to_owned()));
.to_owned()
)
);
let expect_bracket_or_comma = TimelineQuery::parse(r#"lang in [en ["#).unwrap_err(); let expect_bracket_or_comma = TimelineQuery::parse(r#"lang in [en ["#).unwrap_err();
assert_eq!( assert_eq!(
+137 -122
View File
@@ -1,9 +1,3 @@
use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
safe_string::SafeString, schema::users, search::Searcher, timeline::Timeline, Connection,
Error, PlumeRocket, Result, ITEMS_PER_PAGE,
};
use activitypub::{ use activitypub::{
activity::Delete, activity::Delete,
actor::Person, actor::Person,
@@ -11,6 +5,7 @@ use activitypub::{
object::{Image, Tombstone}, object::{Image, Tombstone},
Activity, CustomObject, Endpoint, Activity, CustomObject, Endpoint,
}; };
use bcrypt;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use openssl::{ use openssl::{
@@ -19,15 +14,13 @@ use openssl::{
rsa::Rsa, rsa::Rsa,
sign, sign,
}; };
use plume_common::{ use plume_common::activity_pub::{
activity_pub::{ ap_accept_header,
ap_accept_header, inbox::{AsActor, AsObject, FromId},
inbox::{AsActor, AsObject, FromId}, sign::{gen_keypair, Signer},
sign::{gen_keypair, Signer}, ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
},
utils,
}; };
use plume_common::utils;
use reqwest::{ use reqwest::{
header::{HeaderValue, ACCEPT}, header::{HeaderValue, ACCEPT},
ClientBuilder, ClientBuilder,
@@ -36,6 +29,7 @@ use rocket::{
outcome::IntoOutcome, outcome::IntoOutcome,
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
}; };
use serde_json;
use std::{ use std::{
cmp::PartialEq, cmp::PartialEq,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
@@ -43,6 +37,23 @@ use std::{
use url::Url; use url::Url;
use webfinger::*; use webfinger::*;
use blogs::Blog;
use db_conn::DbConn;
use follows::Follow;
use instance::*;
use medias::Media;
use notifications::Notification;
use post_authors::PostAuthor;
use posts::Post;
use safe_string::SafeString;
use schema::users;
use search::Searcher;
use timeline::Timeline;
use {
ap_url, blocklisted_emails::BlocklistedEmail, Connection, Error, PlumeRocket, Result,
ITEMS_PER_PAGE,
};
pub type CustomPerson = CustomObject<ApSignature, Person>; pub type CustomPerson = CustomObject<ApSignature, Person>;
pub enum Role { pub enum Role {
@@ -52,7 +63,6 @@ pub enum Role {
} }
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)] #[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
pub username: String, pub username: String,
@@ -129,7 +139,7 @@ impl User {
} }
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> { pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
use crate::schema::post_authors; use schema::post_authors;
for blog in Blog::find_for_author(conn, self)? for blog in Blog::find_for_author(conn, self)?
.iter() .iter()
@@ -189,7 +199,7 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub async fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> { pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
let from_db = users::table let from_db = users::table
.filter(users::fqn.eq(fqn)) .filter(users::fqn.eq(fqn))
.first(&*c.conn) .first(&*c.conn)
@@ -197,13 +207,19 @@ impl User {
if let Some(from_db) = from_db { if let Some(from_db) = from_db {
Ok(from_db) Ok(from_db)
} else { } else {
User::fetch_from_webfinger(c, fqn).await User::fetch_from_webfinger(c, fqn)
} }
} }
async fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> { pub fn find_first_local(conn: &Connection) -> Result<User> {
let link = resolve(acct.to_owned(), true) users::table
.await? .filter(users::instance_id.eq(Instance::get_local()?.id))
.first(&*conn)
.map_err(|_| Error::NotFound)
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
@@ -211,9 +227,8 @@ impl User {
User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e) User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e)
} }
pub async fn fetch_remote_interact_uri(acct: &str) -> Result<String> { pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
resolve(acct.to_owned(), true) resolve(acct.to_owned(), true)?
.await?
.links .links
.into_iter() .into_iter()
.find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe") .find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe")
@@ -221,23 +236,29 @@ impl User {
.ok_or(Error::Webfinger) .ok_or(Error::Webfinger)
} }
async fn fetch(url: &str) -> Result<CustomPerson> { fn fetch(url: &str, lu: User) -> Result<CustomPerson> {
let res = ClientBuilder::new() let mut headers = plume_common::activity_pub::request::headers();
.connect_timeout(std::time::Duration::from_secs(5)) headers.insert(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
);
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()? .build()?
.get(url) .get(url)
.headers(headers.clone())
.header( .header(
ACCEPT, "Signature",
HeaderValue::from_str( plume_common::activity_pub::request::signature(&lu, &headers).expect(""),
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
) )
.send() .send()?;
.await?;
let text = &res.text().await?; let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized // without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?; let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let mut json = serde_json::from_str::<CustomPerson>(text)?; let mut json = serde_json::from_str::<CustomPerson>(text)?;
@@ -245,48 +266,49 @@ impl User {
Ok(json) Ok(json)
} }
pub async fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> { pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
let json = User::fetch(url).await?; User::fetch(url, User::find_first_local(&*c.conn)?)
User::from_activity(c, json) .and_then(|json| User::from_activity(c, json))
} }
pub async fn refetch(&self, conn: &Connection) -> Result<()> { pub fn refetch(&self, conn: &Connection) -> Result<()> {
let json = User::fetch(&self.ap_url.clone()).await?; User::fetch(&self.ap_url.clone(), User::find_first_local(conn)?).and_then(|json| {
let avatar = Media::save_remote( let avatar = Media::save_remote(
conn, conn,
json.object json.object
.object_props .object_props
.icon_image()? .icon_image()?
.object_props .object_props
.url_string()?, .url_string()?,
&self, &self,
) )
.ok(); .ok();
diesel::update(self) diesel::update(self)
.set(( .set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?), users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
users::display_name.eq(json.object.object_props.name_string()?), users::display_name.eq(json.object.object_props.name_string()?),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?), users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?), users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::summary.eq(SafeString::new( users::summary.eq(SafeString::new(
&json &json
.object .object
.object_props .object_props
.summary_string() .summary_string()
.unwrap_or_default(), .unwrap_or_default(),
)), )),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?), users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
users::avatar_id.eq(avatar.map(|a| a.id)), users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()), users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json users::public_key.eq(json
.custom_props .custom_props
.public_key_publickey()? .public_key_publickey()?
.public_key_pem_string()?), .public_key_pem_string()?),
)) ))
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
.map_err(Error::from) .map_err(Error::from)
})
} }
pub fn hash_pass(pass: &str) -> Result<String> { pub fn hash_pass(pass: &str) -> Result<String> {
@@ -357,10 +379,9 @@ impl User {
.set_part_of_link(Id::new(&self.outbox_url))?; .set_part_of_link(Id::new(&self.outbox_url))?;
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
} }
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
async fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> { let mut res = ClientBuilder::new()
let res = ClientBuilder::new() .connect_timeout(Some(std::time::Duration::from_secs(5)))
.connect_timeout(std::time::Duration::from_secs(5))
.build()? .build()?
.get(url) .get(url)
.header( .header(
@@ -372,9 +393,8 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"] let items = json["items"]
.as_array() .as_array()
@@ -389,9 +409,9 @@ impl User {
}; };
Ok((items, next)) Ok((items, next))
} }
pub async fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> { pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let res = ClientBuilder::new() let mut res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5)) .connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()? .build()?
.get(&self.outbox_url[..]) .get(&self.outbox_url[..])
.header( .header(
@@ -403,14 +423,13 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") { if let Some(first) = json.get("first") {
let mut items: Vec<T> = Vec::new(); let mut items: Vec<T> = Vec::new();
let mut next = first.as_str().unwrap().to_owned(); let mut next = first.as_str().unwrap().to_owned();
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next).await { while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) {
if page.is_empty() { if page.is_empty() {
break; break;
} }
@@ -435,9 +454,9 @@ impl User {
} }
} }
pub async fn fetch_followers_ids(&self) -> Result<Vec<String>> { pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let res = ClientBuilder::new() let mut res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5)) .connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()? .build()?
.get(&self.followers_endpoint[..]) .get(&self.followers_endpoint[..])
.header( .header(
@@ -449,9 +468,8 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"] Ok(json["items"]
.as_array() .as_array()
@@ -461,8 +479,8 @@ impl User {
.collect::<Vec<String>>()) .collect::<Vec<String>>())
} }
fn get_activities_count(&self, conn: &Connection) -> i64 { fn get_activities_count(&self, conn: &Connection) -> i64 {
use crate::schema::post_authors; use schema::post_authors;
use crate::schema::posts; use schema::posts;
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
posts::table posts::table
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
@@ -476,8 +494,8 @@ impl User {
conn: &Connection, conn: &Connection,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<Vec<serde_json::Value>> { ) -> Result<Vec<serde_json::Value>> {
use crate::schema::post_authors; use schema::post_authors;
use crate::schema::posts; use schema::posts;
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
let posts = posts::table let posts = posts::table
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
@@ -497,7 +515,7 @@ impl User {
} }
pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> { pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::follows; use schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id); let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table users::table
.filter(users::id.eq_any(follows)) .filter(users::id.eq_any(follows))
@@ -506,7 +524,7 @@ impl User {
} }
pub fn count_followers(&self, conn: &Connection) -> Result<i64> { pub fn count_followers(&self, conn: &Connection) -> Result<i64> {
use crate::schema::follows; use schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id); let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table users::table
.filter(users::id.eq_any(follows)) .filter(users::id.eq_any(follows))
@@ -520,7 +538,7 @@ impl User {
conn: &Connection, conn: &Connection,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<Vec<User>> { ) -> Result<Vec<User>> {
use crate::schema::follows; use schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id); let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table users::table
.filter(users::id.eq_any(follows)) .filter(users::id.eq_any(follows))
@@ -531,7 +549,7 @@ impl User {
} }
pub fn get_followed(&self, conn: &Connection) -> Result<Vec<User>> { pub fn get_followed(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::follows::dsl::*; use schema::follows::dsl::*;
let f = follows.filter(follower_id.eq(self.id)).select(following_id); let f = follows.filter(follower_id.eq(self.id)).select(following_id);
users::table users::table
.filter(users::id.eq_any(f)) .filter(users::id.eq_any(f))
@@ -540,7 +558,7 @@ impl User {
} }
pub fn count_followed(&self, conn: &Connection) -> Result<i64> { pub fn count_followed(&self, conn: &Connection) -> Result<i64> {
use crate::schema::follows; use schema::follows;
follows::table follows::table
.filter(follows::follower_id.eq(self.id)) .filter(follows::follower_id.eq(self.id))
.count() .count()
@@ -553,7 +571,7 @@ impl User {
conn: &Connection, conn: &Connection,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<Vec<User>> { ) -> Result<Vec<User>> {
use crate::schema::follows; use schema::follows;
let follows = follows::table let follows = follows::table
.filter(follows::follower_id.eq(self.id)) .filter(follows::follower_id.eq(self.id))
.select(follows::following_id) .select(follows::following_id)
@@ -566,7 +584,7 @@ impl User {
} }
pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result<bool> { pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result<bool> {
use crate::schema::follows; use schema::follows;
follows::table follows::table
.filter(follows::follower_id.eq(other_id)) .filter(follows::follower_id.eq(other_id))
.filter(follows::following_id.eq(self.id)) .filter(follows::following_id.eq(self.id))
@@ -577,7 +595,7 @@ impl User {
} }
pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result<bool> { pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result<bool> {
use crate::schema::follows; use schema::follows;
follows::table follows::table
.filter(follows::follower_id.eq(self.id)) .filter(follows::follower_id.eq(self.id))
.filter(follows::following_id.eq(other_id)) .filter(follows::following_id.eq(other_id))
@@ -588,7 +606,7 @@ impl User {
} }
pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result<bool> { pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result<bool> {
use crate::schema::likes; use schema::likes;
likes::table likes::table
.filter(likes::post_id.eq(post.id)) .filter(likes::post_id.eq(post.id))
.filter(likes::user_id.eq(self.id)) .filter(likes::user_id.eq(self.id))
@@ -599,7 +617,7 @@ impl User {
} }
pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result<bool> { pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result<bool> {
use crate::schema::reshares; use schema::reshares;
reshares::table reshares::table
.filter(reshares::post_id.eq(post.id)) .filter(reshares::post_id.eq(post.id))
.filter(reshares::user_id.eq(self.id)) .filter(reshares::user_id.eq(self.id))
@@ -610,7 +628,7 @@ impl User {
} }
pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result<bool> { pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result<bool> {
use crate::schema::blog_authors; use schema::blog_authors;
blog_authors::table blog_authors::table
.filter(blog_authors::author_id.eq(self.id)) .filter(blog_authors::author_id.eq(self.id))
.filter(blog_authors::blog_id.eq(blog.id)) .filter(blog_authors::blog_id.eq(blog.id))
@@ -765,7 +783,7 @@ impl User {
mime_type: None, mime_type: None,
href: None, href: None,
template: Some(format!( template: Some(format!(
"https://{}/remote_interact?target={{uri}}", "https://{}/remote_interact?{{uri}}",
self.get_instance(conn)?.public_domain self.get_instance(conn)?.public_domain
)), )),
}, },
@@ -794,12 +812,11 @@ impl User {
} }
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for User { impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> {
let conn = try_outcome!(DbConn::from_request(request).await); let conn = request.guard::<DbConn>()?;
request request
.cookies() .cookies()
.get_private(AUTH_COOKIE) .get_private(AUTH_COOKIE)
@@ -811,7 +828,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
impl IntoId for User { impl IntoId for User {
fn into_id(self) -> Id { fn into_id(self) -> Id {
Id::new(self.ap_url) Id::new(self.ap_url.clone())
} }
} }
@@ -1031,13 +1048,11 @@ impl NewUser {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{
instance::{tests as instance_tests, Instance},
search::tests::get_searcher,
tests::{db, rockets},
Connection as Conn,
};
use diesel::Connection; use diesel::Connection;
use instance::{tests as instance_tests, Instance};
use search::tests::get_searcher;
use tests::{db, rockets};
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<User> { pub(crate) fn fill_database(conn: &Conn) -> Vec<User> {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
+4
View File
@@ -1,3 +1,7 @@
extern crate diesel;
extern crate plume_common;
extern crate plume_models;
use diesel::Connection; use diesel::Connection;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use plume_models::migrations::IMPORTED_MIGRATIONS; use plume_models::migrations::IMPORTED_MIGRATIONS;
+1 -1
View File
@@ -10,7 +10,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Language: fr\n" "X-Crowdin-Language: fr\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
+1 -2
View File
@@ -10,8 +10,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ar\n" "X-Crowdin-Language: ar\n"
"X-Crowdin-File: /master/po/plume/plume.pot\n" "X-Crowdin-File: /master/po/plume/plume.pot\n"
+1 -1
View File
@@ -10,7 +10,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Language: fr\n" "X-Crowdin-Language: fr\n"
"X-Crowdin-File: /master/po/plume/plume.pot\n" "X-Crowdin-File: /master/po/plume/plume.pot\n"
+1 -1
View File
@@ -1 +1 @@
nightly-2020-05-05 nightly-2019-03-23
+2 -2
View File
@@ -24,9 +24,9 @@ if [ $ARCH == "aarch64" -o $ARCH == "armv71" ] ; then
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
mkdir -p /scratch/src mkdir -p /scratch/src
cd /scratch/src cd /scratch/src
svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_900/final/ llvm svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_800/final/ llvm
cd /scratch/src/llvm/tools cd /scratch/src/llvm/tools
svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_900/final/ lld svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_800/final/ lld
mkdir -p /scratch/build/arm mkdir -p /scratch/build/arm
cd /scratch/build/arm cd /scratch/build/arm
if [ "$ARCH" == "aarch64" ] ; then if [ "$ARCH" == "aarch64" ] ; then
+3 -4
View File
@@ -8,7 +8,7 @@ description: |
* Media management: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume. * Media management: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
* Federation: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called instances) have their own rules and thematics, but they can all communicate with each other. * Federation: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called instances) have their own rules and thematics, but they can all communicate with each other.
* Collaborative writing: invite other people to your blogs, and write articles together. * Collaborative writing: invite other people to your blogs, and write articles together.
grade: stable grade: devel # must be 'stable' to release into candidate/stable channels
confinement: strict confinement: strict
apps: apps:
@@ -25,13 +25,12 @@ parts:
plume: plume:
plugin: rust plugin: rust
source: . source: .
rust-revision: nightly-2020-01-15 rust-revision: nightly-2019-03-23
build-packages: build-packages:
- libssl-dev - libssl-dev
- pkg-config - pkg-config
- libsqlite3-dev - libsqlite3-dev
- gettext - gettext
- libclang-8-dev
- on arm64,armhf,ppc64el,s390x: - on arm64,armhf,ppc64el,s390x:
- lld-8 - lld-8
override-build: | override-build: |
@@ -43,7 +42,7 @@ parts:
# Only Tier 1 Rust platforms get rust-lld # Only Tier 1 Rust platforms get rust-lld
# On the others (arm64, armhf, powerpc64, s390x) fall back to using # On the others (arm64, armhf, powerpc64, s390x) fall back to using
# the system LLD we've installed earlier. # the system LLD we've installed earlier.
case ${SNAPCRAFT_ARCH_TRIPLET} in \ case ${SNAPCRAFT_ARCH_TRIPLE} in \
aarch64-linux-gnu|arm-linux-gnueabihf|powerpc64-linux-gnu|s390x-linux-gnu) \ aarch64-linux-gnu|arm-linux-gnueabihf|powerpc64-linux-gnu|s390x-linux-gnu) \
RUSTFLAGS="-C linker=lld-8" cargo web deploy -p plume-front --release \ RUSTFLAGS="-C linker=lld-8" cargo web deploy -p plume-front --release \
;; \ ;; \
+1 -3
View File
@@ -35,7 +35,6 @@ impl Scope for plume_models::posts::Post {
pub struct Authorization<A, S>(pub ApiToken, PhantomData<(A, S)>); pub struct Authorization<A, S>(pub ApiToken, PhantomData<(A, S)>);
#[rocket::async_trait]
impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S> impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S>
where where
A: Action, A: Action,
@@ -43,10 +42,9 @@ where
{ {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> {
request request
.guard::<ApiToken>() .guard::<ApiToken>()
.await
.map_failure(|_| (Status::Unauthorized, ())) .map_failure(|_| (Status::Unauthorized, ()))
.and_then(|token| { .and_then(|token| {
if token.can(A::to_str(), S::to_str()) { if token.can(A::to_str(), S::to_str()) {
+16 -25
View File
@@ -4,6 +4,7 @@ use rocket::{
response::{self, Responder}, response::{self, Responder},
}; };
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket}; use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket};
@@ -25,31 +26,21 @@ impl From<std::option::NoneError> for ApiError {
} }
} }
#[rocket::async_trait]
impl<'r> Responder<'r> for ApiError { impl<'r> Responder<'r> for ApiError {
async fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, req: &Request) -> response::Result<'r> {
match self.0 { match self.0 {
Error::NotFound => { Error::NotFound => Json(json!({
Json(json!({ "error": "Not found"
"error": "Not found" }))
})) .respond_to(req),
.respond_to(req) Error::Unauthorized => Json(json!({
.await "error": "You are not authorized to access this resource"
} }))
Error::Unauthorized => { .respond_to(req),
Json(json!({ _ => Json(json!({
"error": "You are not authorized to access this resource" "error": "Server error"
})) }))
.respond_to(req) .respond_to(req),
.await
}
_ => {
Json(json!({
"error": "Server error"
}))
.respond_to(req)
.await
}
} }
} }
} }
@@ -64,14 +55,14 @@ pub struct OAuthRequest {
} }
#[get("/oauth2?<query..>")] #[get("/oauth2?<query..>")]
pub async fn oauth( pub fn oauth(
query: Form<OAuthRequest>, query: Form<OAuthRequest>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let app = App::find_by_client_id(conn, &query.client_id)?; let app = App::find_by_client_id(conn, &query.client_id)?;
if app.client_secret == query.client_secret { if app.client_secret == query.client_secret {
if let Ok(user) = User::find_by_fqn(&rockets, &query.username).await { if let Ok(user) = User::find_by_fqn(&rockets, &query.username) {
if user.auth(&query.password) { if user.auth(&query.password) {
let token = ApiToken::insert( let token = ApiToken::insert(
conn, conn,
+2 -2
View File
@@ -98,7 +98,7 @@ pub fn list(
} }
#[post("/posts", data = "<payload>")] #[post("/posts", data = "<payload>")]
pub async fn create( pub fn create(
auth: Authorization<Write, Post>, auth: Authorization<Write, Post>,
payload: Json<NewPostData>, payload: Json<NewPostData>,
rockets: PlumeRocket, rockets: PlumeRocket,
@@ -192,7 +192,7 @@ pub async fn create(
for m in mentions.into_iter() { for m in mentions.into_iter() {
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&rockets, &m).await?, &Mention::build_activity(&rockets, &m)?,
post.id, post.id,
true, true,
true, true,
+24 -24
View File
@@ -9,12 +9,12 @@ use plume_models::{
use rocket::{data::*, http::Status, response::status, Outcome::*, Request}; use rocket::{data::*, http::Status, response::status, Outcome::*, Request};
use rocket_contrib::json::*; use rocket_contrib::json::*;
use serde::Deserialize; use serde::Deserialize;
use tokio::io::AsyncReadExt; use std::io::Read;
pub async fn handle_incoming( pub fn handle_incoming(
rockets: PlumeRocket, rockets: PlumeRocket,
data: SignedJson<serde_json::Value>, data: SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers,
) -> Result<String, status::BadRequest<&'static str>> { ) -> Result<String, status::BadRequest<&'static str>> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let act = data.1.into_inner(); let act = data.1.into_inner();
@@ -32,7 +32,6 @@ pub async fn handle_incoming(
// maybe we just know an old key? // maybe we just know an old key?
actor actor
.refetch(conn) .refetch(conn)
.await
.and_then(|_| User::get(conn, actor.id)) .and_then(|_| User::get(conn, actor.id))
.and_then(|u| { .and_then(|u| {
if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) { if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) {
@@ -74,31 +73,32 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> {
type Owned = String; type Owned = String;
type Borrowed = str; type Borrowed = str;
fn transform<'r>(r: &'r Request, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { fn transform(
Box::pin(async move { r: &Request,
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT); d: Data,
let mut s = String::with_capacity(512); ) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
let outcome = match d.open().take(size_limit).read_to_string(&mut s).await { let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
Ok(_) => Success(s), let mut s = String::with_capacity(512);
Err(e) => Failure((Status::BadRequest, JsonError::Io(e))), match d.open().take(size_limit).read_to_string(&mut s) {
}; Ok(_) => Transform::Borrowed(Success(s)),
Transform::Borrowed(outcome) Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))),
}) }
} }
fn from_data( fn from_data(
_: &Request<'_>, _: &Request,
o: Transformed<'a, Self>, o: Transformed<'a, Self>,
) -> FromDataFuture<'a, Self, Self::Error> { ) -> rocket::data::Outcome<Self, Self::Error> {
Box::pin(async move { let string = o.borrowed()?;
let string = try_outcome!(o.borrowed()); match serde_json::from_str(&string) {
match serde_json::from_str(&string) { Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))),
Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))), Err(e) => {
Err(e) if e.is_data() => { if e.is_data() {
return Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
} else {
Failure((Status::BadRequest, JsonError::Parse(string, e)))
} }
Err(e) => Failure((Status::BadRequest, JsonError::Parse(string, e))),
} }
}) }
} }
} }
+61 -9
View File
@@ -1,15 +1,43 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
#![feature(proc_macro_hygiene, try_trait)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
extern crate activitypub;
extern crate askama_escape;
extern crate atom_syndication;
extern crate chrono;
extern crate clap;
extern crate colored;
extern crate ctrlc;
extern crate diesel;
extern crate dotenv;
#[macro_use] #[macro_use]
extern crate gettext_macros; extern crate gettext_macros;
extern crate gettext_utils;
extern crate guid_create;
extern crate heck;
extern crate lettre;
extern crate lettre_email;
extern crate multipart;
extern crate num_cpus;
extern crate plume_api;
extern crate plume_common;
extern crate plume_models;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
extern crate rocket_contrib;
extern crate rocket_csrf;
extern crate rocket_i18n;
#[macro_use]
extern crate runtime_fmt;
extern crate scheduled_thread_pool;
extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
extern crate serde_qs;
extern crate validator;
#[macro_use] #[macro_use]
extern crate validator_derive; extern crate validator_derive;
extern crate validator; extern crate webfinger;
use clap::App; use clap::App;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
@@ -20,6 +48,7 @@ use plume_models::{
search::{Searcher as UnmanagedSearcher, SearcherError}, search::{Searcher as UnmanagedSearcher, SearcherError},
Connection, Error, CONFIG, Connection, Error, CONFIG,
}; };
use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use std::process::exit; use std::process::exit;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -53,13 +82,10 @@ fn init_pool() -> Option<DbPool> {
} }
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str()); let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
let mut builder = DbPool::builder() let pool = DbPool::builder()
.connection_customizer(Box::new(PragmaForeignKey)) .connection_customizer(Box::new(PragmaForeignKey))
.min_idle(CONFIG.db_min_idle); .build(manager)
if let Some(max_size) = CONFIG.db_max_size { .ok()?;
builder = builder.max_size(max_size);
};
let pool = builder.build(manager).ok()?;
Instance::cache_local(&pool.get().unwrap()); Instance::cache_local(&pool.get().unwrap());
Some(pool) Some(pool)
} }
@@ -273,7 +299,33 @@ Then try to restart Plume
.manage(dbpool) .manage(dbpool)
.manage(Arc::new(workpool)) .manage(Arc::new(workpool))
.manage(searcher) .manage(searcher)
.manage(include_i18n!()); .manage(include_i18n!())
.attach(
CsrfFairingBuilder::new()
.set_default_target(
"/csrf-violation?target=<uri>".to_owned(),
rocket::http::Method::Post,
)
.add_exceptions(vec![
(
"/inbox".to_owned(),
"/inbox".to_owned(),
rocket::http::Method::Post,
),
(
"/@/<name>/inbox".to_owned(),
"/@/<name>/inbox".to_owned(),
rocket::http::Method::Post,
),
(
"/api/<path..>".to_owned(),
"/api/<path..>".to_owned(),
rocket::http::Method::Post,
),
])
.finalize()
.expect("main: csrf fairing creation error"),
);
#[cfg(feature = "test")] #[cfg(feature = "test")]
let rocket = rocket.mount("/test", routes![test_routes::health,]); let rocket = rocket.mount("/test", routes![test_routes::health,]);
+36 -41
View File
@@ -1,4 +1,5 @@
use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; use activitypub::collection::{OrderedCollection, OrderedCollectionPage};
use atom_syndication::{Entry, FeedBuilder};
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
use rocket::{ use rocket::{
http::ContentType, http::ContentType,
@@ -9,24 +10,20 @@ use rocket_i18n::I18n;
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use crate::routes::{errors::ErrorPage, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{ActivityStream, ApRequest}; use plume_common::activity_pub::{ActivityStream, ApRequest};
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString, blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
users::User, Connection, PlumeRocket, users::User, Connection, PlumeRocket,
}; };
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
use template_utils::{IntoContext, Ructe};
#[get("/~/<name>?<page>", rank = 2)] #[get("/~/<name>?<page>", rank = 2)]
pub async fn details( pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
name: String,
page: Option<Page>,
rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name).await?; let blog = Blog::find_by_fqn(&rockets, &name)?;
let posts = Post::blog_page(conn, &blog, page.limits())?; let posts = Post::blog_page(conn, &blog, page.limits())?;
let articles_count = Post::count_for_blog(conn, &blog)?; let articles_count = Post::count_for_blog(conn, &blog)?;
let authors = &blog.list_authors(conn)?; let authors = &blog.list_authors(conn)?;
@@ -42,12 +39,12 @@ pub async fn details(
} }
#[get("/~/<name>", rank = 1)] #[get("/~/<name>", rank = 1)]
pub async fn activity_details( pub fn activity_details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> { ) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_by_fqn(&rockets, &name).await?; let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?)) Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
} }
@@ -87,7 +84,7 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/blogs/new", data = "<form>")] #[post("/blogs/new", data = "<form>")]
pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect { pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
let slug = utils::make_actor_id(&form.title); let slug = utils::make_actor_id(&form.title);
let conn = &*rockets.conn; let conn = &*rockets.conn;
let intl = &rockets.intl.catalog; let intl = &rockets.intl.catalog;
@@ -97,7 +94,7 @@ pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Res
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e, Err(e) => e,
}; };
if Blog::find_by_fqn(&rockets, &slug).await.is_ok() { if Blog::find_by_fqn(&rockets, &slug).is_ok() {
errors.add( errors.add(
"title", "title",
ValidationError { ValidationError {
@@ -140,18 +137,16 @@ pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Res
.expect("blog::create: author error"); .expect("blog::create: author error");
Flash::success( Flash::success(
Redirect::to(uri!(details: name = slug, page = _)), Redirect::to(uri!(details: name = slug.clone(), page = _)),
&i18n!(intl, "Your blog was successfully created!"), &i18n!(intl, "Your blog was successfully created!"),
) )
.into() .into()
} }
#[post("/~/<name>/delete")] #[post("/~/<name>/delete")]
pub async fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect { pub fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name) let blog = Blog::find_by_fqn(&rockets, &name).expect("blog::delete: blog not found");
.await
.expect("blog::delete: blog not found");
if rockets if rockets
.user .user
@@ -190,9 +185,9 @@ pub struct EditForm {
} }
#[get("/~/<name>/edit")] #[get("/~/<name>/edit")]
pub async fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name).await?; let blog = Blog::find_by_fqn(&rockets, &name)?;
if rockets if rockets
.user .user
.clone() .clone()
@@ -239,16 +234,14 @@ fn check_media(conn: &Connection, id: i32, user: &User) -> bool {
} }
#[put("/~/<name>/edit", data = "<form>")] #[put("/~/<name>/edit", data = "<form>")]
pub async fn update( pub fn update(
name: String, name: String,
form: LenientForm<EditForm>, form: LenientForm<EditForm>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let intl = &rockets.intl.catalog; let intl = &rockets.intl.catalog;
let mut blog = Blog::find_by_fqn(&rockets, &name) let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
.await
.expect("blog::update: blog not found");
if !rockets if !rockets
.user .user
.clone() .clone()
@@ -350,36 +343,38 @@ pub async fn update(
} }
#[get("/~/<name>/outbox")] #[get("/~/<name>/outbox")]
pub async fn outbox( pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> {
name: String, let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollection>> {
let blog = Blog::find_by_fqn(&rockets, &name).await?;
Some(blog.outbox(&*rockets.conn).ok()?) Some(blog.outbox(&*rockets.conn).ok()?)
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[get("/~/<name>/outbox?<page>")] #[get("/~/<name>/outbox?<page>")]
pub async fn outbox_page( pub fn outbox_page(
name: String, name: String,
page: Page, page: Page,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollectionPage>> { ) -> Option<ActivityStream<OrderedCollectionPage>> {
let blog = Blog::find_by_fqn(&rockets, &name).await?; let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
Some(blog.outbox_page(&*rockets.conn, page.limits()).ok()?) Some(blog.outbox_page(&*rockets.conn, page.limits()).ok()?)
} }
#[get("/~/<name>/atom.xml")] #[get("/~/<name>/atom.xml")]
pub async fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> { pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&rockets, &name).await?; let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
let conn = &*rockets.conn; let conn = &*rockets.conn;
let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?; let feed = FeedBuilder::default()
let uri = Instance::get_local() .title(blog.title.clone())
.ok()? .id(Instance::get_local()
.compute_box("~", &name, "atom.xml"); .ok()?
let title = &blog.title; .compute_box("~", &name, "atom.xml"))
let default_updated = &blog.creation_date; .entries(
let feed = super::build_atom_feed(entries, &uri, title, default_updated, conn); Post::get_recents_for_blog(&*conn, &blog, 15)
.ok()?
.into_iter()
.map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>(),
)
.build()
.ok()?;
Some(Content( Some(Content(
ContentType::new("application", "atom+xml"), ContentType::new("application", "atom+xml"),
feed.to_string(), feed.to_string(),
+20 -26
View File
@@ -1,15 +1,13 @@
use crate::template_utils::Ructe;
use activitypub::object::Note; use activitypub::object::Note;
use rocket::{ use rocket::{
request::LenientForm, request::LenientForm,
response::{Flash, Redirect}, response::{Flash, Redirect},
}; };
use template_utils::Ructe;
use validator::Validate; use validator::Validate;
use std::time::Duration; use std::time::Duration;
use crate::routes::errors::ErrorPage;
use crate::template_utils::IntoContext;
use plume_common::{ use plume_common::{
activity_pub::{broadcast, ActivityStream, ApRequest}, activity_pub::{broadcast, ActivityStream, ApRequest},
utils, utils,
@@ -18,17 +16,19 @@ use plume_models::{
blogs::Blog, comments::*, inbox::inbox, instance::Instance, medias::Media, mentions::Mention, blogs::Blog, comments::*, inbox::inbox, instance::Instance, medias::Media, mentions::Mention,
posts::Post, safe_string::SafeString, tags::Tag, users::User, Error, PlumeRocket, posts::Post, safe_string::SafeString, tags::Tag, users::User, Error, PlumeRocket,
}; };
use routes::errors::ErrorPage;
use template_utils::IntoContext;
#[derive(Default, FromForm, Debug, Validate)] #[derive(Default, FromForm, Debug, Validate)]
pub struct NewCommentForm { pub struct NewCommentForm {
pub responding_to: Option<i32>, pub responding_to: Option<i32>,
#[validate(length(min = 1, message = "Your comment can't be empty"))] #[validate(length(min = "1", message = "Your comment can't be empty"))]
pub content: String, pub content: String,
pub warning: String, pub warning: String,
} }
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")] #[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
pub async fn create( pub fn create(
blog_name: String, blog_name: String,
slug: String, slug: String,
form: LenientForm<NewCommentForm>, form: LenientForm<NewCommentForm>,
@@ -36,12 +36,10 @@ pub async fn create(
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, Ructe> { ) -> Result<Flash<Redirect>, Ructe> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &blog_name) let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("comments::create: blog error");
.await
.expect("comments::create: blog error");
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error"); let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
match form.validate() { form.validate()
Ok(_ok) => { .map(|_| {
let (html, mentions, _hashtags) = utils::md_to_html( let (html, mentions, _hashtags) = utils::md_to_html(
form.content.as_ref(), form.content.as_ref(),
Some( Some(
@@ -68,7 +66,6 @@ pub async fn create(
.expect("comments::create: insert error"); .expect("comments::create: insert error");
let new_comment = comm let new_comment = comm
.create_activity(&rockets) .create_activity(&rockets)
.await
.expect("comments::create: activity error"); .expect("comments::create: activity error");
// save mentions // save mentions
@@ -76,7 +73,6 @@ pub async fn create(
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&rockets, &ment) &Mention::build_activity(&rockets, &ment)
.await
.expect("comments::create: build mention error"), .expect("comments::create: build mention error"),
comm.id, comm.id,
false, false,
@@ -94,14 +90,14 @@ pub async fn create(
.worker .worker
.execute(move || broadcast(&user_clone, new_comment, dest)); .execute(move || broadcast(&user_clone, new_comment, dest));
Ok(Flash::success( Flash::success(
Redirect::to( Redirect::to(
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _), uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
), ),
i18n!(&rockets.intl.catalog, "Your comment has been posted."), i18n!(&rockets.intl.catalog, "Your comment has been posted."),
)) )
} })
Err(errors) => { .map_err(|errors| {
// TODO: de-duplicate this code // TODO: de-duplicate this code
let comments = CommentTree::from_post(&*conn, &post, Some(&user)) let comments = CommentTree::from_post(&*conn, &post, Some(&user))
.expect("comments::create: comments error"); .expect("comments::create: comments error");
@@ -110,7 +106,7 @@ pub async fn create(
.responding_to .responding_to
.and_then(|r| Comment::get(&*conn, r).ok()); .and_then(|r| Comment::get(&*conn, r).ok());
Err(render!(posts::details( render!(posts::details(
&rockets.to_context(), &rockets.to_context(),
post.clone(), post.clone(),
blog, blog,
@@ -137,9 +133,8 @@ pub async fn create(
post.get_authors(&*conn) post.get_authors(&*conn)
.expect("comments::create: authors error")[0] .expect("comments::create: authors error")[0]
.clone() .clone()
))) ))
} })
}
} }
#[post("/~/<blog>/<slug>/comment/<id>/delete")] #[post("/~/<blog>/<slug>/comment/<id>/delete")]
@@ -179,16 +174,15 @@ pub fn delete(
} }
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
pub async fn activity_pub( pub fn activity_pub(
_blog: String, _blog: String,
_slug: String, _slug: String,
id: i32, id: i32,
_ap: ApRequest, _ap: ApRequest,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Option<ActivityStream<Note>> { ) -> Option<ActivityStream<Note>> {
let c = match Comment::get(&*rockets.conn, id) { Comment::get(&*rockets.conn, id)
Ok(c) => c.to_activity(&rockets).await.ok(), .and_then(|c| c.to_activity(&rockets))
Err(_) => None, .ok()
}; .map(ActivityStream::new)
c.map(ActivityStream::new)
} }
+12 -24
View File
@@ -1,10 +1,9 @@
use crate::template_utils::{IntoContext, Ructe};
use plume_models::{Error, PlumeRocket}; use plume_models::{Error, PlumeRocket};
use rocket::{ use rocket::{
request::FromRequest,
response::{self, Responder}, response::{self, Responder},
Request, Request,
}; };
use template_utils::{IntoContext, Ructe};
#[derive(Debug)] #[derive(Debug)]
pub struct ErrorPage(Error); pub struct ErrorPage(Error);
@@ -15,46 +14,35 @@ impl From<Error> for ErrorPage {
} }
} }
#[rocket::async_trait]
impl<'r> Responder<'r> for ErrorPage { impl<'r> Responder<'r> for ErrorPage {
async fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, req: &Request) -> response::Result<'r> {
let rockets = PlumeRocket::from_request(req).await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
match self.0 { match self.0 {
Error::NotFound => { Error::NotFound => render!(errors::not_found(&rockets.to_context())).respond_to(req),
render!(errors::not_found(&rockets.to_context()))
.respond_to(req)
.await
}
Error::Unauthorized => { Error::Unauthorized => {
render!(errors::not_found(&rockets.to_context())) render!(errors::not_found(&rockets.to_context())).respond_to(req)
.respond_to(req)
.await
}
_ => {
render!(errors::not_found(&rockets.to_context()))
.respond_to(req)
.await
} }
_ => render!(errors::not_found(&rockets.to_context())).respond_to(req),
} }
} }
} }
#[catch(404)] #[catch(404)]
pub async fn not_found(req: &Request<'_>) -> Ructe { pub fn not_found(req: &Request) -> Ructe {
let rockets = req.guard::<PlumeRocket>().await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
render!(errors::not_found(&rockets.to_context())) render!(errors::not_found(&rockets.to_context()))
} }
#[catch(422)] #[catch(422)]
pub async fn unprocessable_entity(req: &Request<'_>) -> Ructe { pub fn unprocessable_entity(req: &Request) -> Ructe {
let rockets = req.guard::<PlumeRocket>().await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
render!(errors::unprocessable_entity(&rockets.to_context())) render!(errors::unprocessable_entity(&rockets.to_context()))
} }
#[catch(500)] #[catch(500)]
pub async fn server_error(req: &Request<'_>) -> Ructe { pub fn server_error(req: &Request) -> Ructe {
let rockets = req.guard::<PlumeRocket>().await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
render!(errors::server_error(&rockets.to_context())) render!(errors::server_error(&rockets.to_context()))
} }
+14 -17
View File
@@ -5,13 +5,11 @@ use rocket::{
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use std::path::PathBuf; use serde_json;
use std::str::FromStr; use std::str::FromStr;
use validator::{Validate, ValidationErrors}; use validator::{Validate, ValidationErrors};
use crate::inbox; use inbox;
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, inbox::FromId}; use plume_common::activity_pub::{broadcast, inbox::FromId};
use plume_models::{ use plume_models::{
admin::*, admin::*,
@@ -27,6 +25,8 @@ use plume_models::{
users::{Role, User}, users::{Role, User},
Connection, Error, PlumeRocket, CONFIG, Connection, Error, PlumeRocket, CONFIG,
}; };
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
use template_utils::{IntoContext, Ructe};
#[get("/")] #[get("/")]
pub fn index(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn index(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
@@ -76,12 +76,12 @@ pub fn admin_mod(_mod: Moderator, rockets: PlumeRocket) -> Ructe {
#[derive(Clone, FromForm, Validate)] #[derive(Clone, FromForm, Validate)]
pub struct InstanceSettingsForm { pub struct InstanceSettingsForm {
#[validate(length(min = 1))] #[validate(length(min = "1"))]
pub name: String, pub name: String,
pub open_registrations: bool, pub open_registrations: bool,
pub short_description: SafeString, pub short_description: SafeString,
pub long_description: SafeString, pub long_description: SafeString,
#[validate(length(min = 1))] #[validate(length(min = "1"))]
pub default_license: String, pub default_license: String,
} }
@@ -248,7 +248,7 @@ where
{ {
type Error = (); type Error = ();
fn from_form(items: &mut FormItems<'_>, _strict: bool) -> Result<Self, Self::Error> { fn from_form(items: &mut FormItems, _strict: bool) -> Result<Self, Self::Error> {
let (ids, act) = items.fold((vec![], None), |(mut ids, act), item| { let (ids, act) = items.fold((vec![], None), |(mut ids, act), item| {
let (name, val) = item.key_value_decoded(); let (name, val) = item.key_value_decoded();
@@ -379,28 +379,25 @@ fn ban(
.unwrap(); .unwrap();
let target = User::one_by_instance(&*conn)?; let target = User::one_by_instance(&*conn)?;
let delete_act = u.delete_activity(&*conn)?; let delete_act = u.delete_activity(&*conn)?;
worker.execute(move || broadcast(&u, delete_act, target)); let u_clone = u.clone();
worker.execute(move || broadcast(&u_clone, delete_act, target));
} }
Ok(()) Ok(())
} }
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
pub async fn shared_inbox( pub fn shared_inbox(
rockets: PlumeRocket, rockets: PlumeRocket,
data: inbox::SignedJson<serde_json::Value>, data: inbox::SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers,
) -> Result<String, status::BadRequest<&'static str>> { ) -> Result<String, status::BadRequest<&'static str>> {
inbox::handle_incoming(rockets, data, headers).await inbox::handle_incoming(rockets, data, headers)
} }
#[get("/remote_interact?<target>")] #[get("/remote_interact?<target>")]
pub async fn interact( pub fn interact(rockets: PlumeRocket, user: Option<User>, target: String) -> Option<Redirect> {
rockets: PlumeRocket, if User::find_by_fqn(&rockets, &target).is_ok() {
user: Option<User>,
target: String,
) -> Option<Redirect> {
if User::find_by_fqn(&rockets, &target).await.is_ok() {
return Some(Redirect::to(uri!(super::user::details: name = target))); return Some(Redirect::to(uri!(super::user::details: name = target)));
} }
+1 -1
View File
@@ -1,12 +1,12 @@
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use crate::routes::errors::ErrorPage;
use plume_common::activity_pub::broadcast; use plume_common::activity_pub::broadcast;
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blogs::Blog, inbox::inbox, likes, posts::Post, timeline::*, users::User, Error, PlumeRocket, blogs::Blog, inbox::inbox, likes, posts::Post, timeline::*, users::User, Error, PlumeRocket,
}; };
use routes::errors::ErrorPage;
#[post("/~/<blog>/<slug>/like")] #[post("/~/<blog>/<slug>/like")]
pub fn create( pub fn create(
+2 -2
View File
@@ -1,5 +1,3 @@
use crate::routes::{errors::ErrorPage, Page};
use crate::template_utils::{IntoContext, Ructe};
use guid_create::GUID; use guid_create::GUID;
use multipart::server::{ use multipart::server::{
save::{SaveResult, SavedData}, save::{SaveResult, SavedData},
@@ -12,7 +10,9 @@ use rocket::{
Data, Data,
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use routes::{errors::ErrorPage, Page};
use std::fs; use std::fs;
use template_utils::{IntoContext, Ructe};
#[get("/medias?<page>")] #[get("/medias?<page>")]
pub fn list(user: User, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn list(user: User, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
Executable → Regular
+14 -56
View File
@@ -1,14 +1,11 @@
#![warn(clippy::too_many_arguments)] #![warn(clippy::too_many_arguments)]
use crate::template_utils::Ructe; use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
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 plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
use rocket::{ use rocket::{
http::{ http::{
hyper::header::{CacheControl, CacheDirective, ETag, EntityTag},
uri::{FromUriParam, Query}, uri::{FromUriParam, Query},
Header, RawStr, Status, RawStr, Status,
}, },
request::{self, FromFormValue, FromRequest, Request}, request::{self, FromFormValue, FromRequest, Request},
response::{self, Flash, NamedFile, Redirect, Responder, Response}, response::{self, Flash, NamedFile, Redirect, Responder, Response},
@@ -19,6 +16,7 @@ use std::{
hash::Hasher, hash::Hasher,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use template_utils::Ructe;
/// Special return type used for routes that "cannot fail", and instead /// Special return type used for routes that "cannot fail", and instead
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response /// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
@@ -94,11 +92,10 @@ impl Page {
#[derive(Shrinkwrap)] #[derive(Shrinkwrap)]
pub struct ContentLen(pub u64); pub struct ContentLen(pub u64);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen { impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
type Error = (); type Error = ();
async fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match r.limits().get("forms") { match r.limits().get("forms") {
Some(l) => Outcome::Success(ContentLen(l)), Some(l) => Outcome::Success(ContentLen(l)),
None => Outcome::Failure((Status::InternalServerError, ())), None => Outcome::Failure((Status::InternalServerError, ())),
@@ -118,46 +115,13 @@ pub struct RemoteForm {
pub remote: String, pub remote: String,
} }
pub fn build_atom_feed( pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
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)
.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();
EntryBuilder::default() EntryBuilder::default()
.title(format!("<![CDATA[{}]]>", post.title)) .title(format!("<![CDATA[{}]]>", post.title))
.content( .content(
ContentBuilder::default() ContentBuilder::default()
.value(format!("<![CDATA[{}]]>", *post.content.get())) .value(format!("<![CDATA[{}]]>", *post.content.get()))
.src(post.ap_url.clone())
.content_type("html".to_string()) .content_type("html".to_string())
.build() .build()
.expect("Atom feed: content error"), .expect("Atom feed: content error"),
@@ -175,11 +139,6 @@ fn post_to_atom(post: Post, conn: &Connection) -> Entry {
}) })
.collect::<Vec<Person>>(), .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())
.links(vec![LinkBuilder::default() .links(vec![LinkBuilder::default()
.href(post.ap_url) .href(post.ap_url)
.build() .build()
@@ -208,15 +167,14 @@ pub mod well_known;
#[response()] #[response()]
pub struct CachedFile { pub struct CachedFile {
inner: NamedFile, inner: NamedFile,
cache_control: Header<'static>, cache_control: CacheControl,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ThemeFile(NamedFile); pub struct ThemeFile(NamedFile);
#[rocket::async_trait]
impl<'r> Responder<'r> for ThemeFile { impl<'r> Responder<'r> for ThemeFile {
async fn respond_to(self, r: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, r: &Request) -> response::Result<'r> {
let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?; let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?;
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
@@ -229,12 +187,12 @@ impl<'r> Responder<'r> for ThemeFile {
{ {
Response::build() Response::build()
.status(Status::NotModified) .status(Status::NotModified)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} else { } else {
Response::build() Response::build()
.merge(self.0.respond_to(r).await.ok().unwrap()) .merge(self.0.respond_to(r)?)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} }
} }
@@ -257,7 +215,7 @@ pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
.ok() .ok()
.map(|f| CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: Header::new("Cache-Control", format!("max-age={}", 60 * 60 * 24 * 30)), cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
}) })
} }
#[get("/static/<file..>", rank = 3)] #[get("/static/<file..>", rank = 3)]
@@ -266,6 +224,6 @@ pub fn static_files(file: PathBuf) -> Option<CachedFile> {
.ok() .ok()
.map(|f| CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: Header::new("Cache-Control", format!("max-age={}", 60 * 60 * 24 * 30)), cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
}) })
} }
+2 -2
View File
@@ -1,10 +1,10 @@
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use crate::routes::{errors::ErrorPage, Page};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::utils; use plume_common::utils;
use plume_models::{notifications::Notification, users::User, PlumeRocket}; use plume_models::{notifications::Notification, users::User, PlumeRocket};
use routes::{errors::ErrorPage, Page};
use template_utils::{IntoContext, Ructe};
#[get("/notifications?<page>")] #[get("/notifications?<page>")]
pub fn notifications( pub fn notifications(
+38 -48
View File
@@ -1,4 +1,3 @@
use activitypub::link;
use chrono::Utc; use chrono::Utc;
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use rocket::request::LenientForm; use rocket::request::LenientForm;
@@ -11,10 +10,6 @@ use std::{
}; };
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use crate::routes::{
comments::NewCommentForm, errors::ErrorPage, ContentLen, Page, RemoteForm, RespondOrRedirect,
};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest}; use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest};
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
@@ -32,9 +27,13 @@ use plume_models::{
users::User, users::User,
Error, PlumeRocket, Error, PlumeRocket,
}; };
use routes::{
comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm, RespondOrRedirect,
};
use template_utils::{IntoContext, Ructe};
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)] #[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub async fn details( pub fn details(
blog: String, blog: String,
slug: String, slug: String,
responding_to: Option<i32>, responding_to: Option<i32>,
@@ -42,7 +41,7 @@ pub async fn details(
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = rockets.user.clone(); let user = rockets.user.clone();
let blog = Blog::find_by_fqn(&rockets, &blog).await?; let blog = Blog::find_by_fqn(&rockets, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?; let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if !(post.published if !(post.published
|| post || post
@@ -100,14 +99,14 @@ pub async fn details(
} }
#[get("/~/<blog>/<slug>", rank = 3)] #[get("/~/<blog>/<slug>", rank = 3)]
pub async fn activity_details( pub fn activity_details(
blog: String, blog: String,
slug: String, slug: String,
_ap: ApRequest, _ap: ApRequest,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<ActivityStream<LicensedArticle>, Option<String>> { ) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &blog).await.map_err(|_| None)?; let blog = Blog::find_by_fqn(&rockets, &blog).map_err(|_| None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?; let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
if post.published { if post.published {
Ok(ActivityStream::new( Ok(ActivityStream::new(
@@ -131,9 +130,9 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/~/<blog>/new", rank = 1)] #[get("/~/<blog>/new", rank = 1)]
pub async fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog).await?; let b = Blog::find_by_fqn(&rockets, &blog)?;
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
if !user.is_author_in(&*conn, &b)? { if !user.is_author_in(&*conn, &b)? {
@@ -163,7 +162,7 @@ pub async fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<R
} }
#[get("/~/<blog>/<slug>/edit")] #[get("/~/<blog>/<slug>/edit")]
pub async fn edit( pub fn edit(
blog: String, blog: String,
slug: String, slug: String,
cl: ContentLen, cl: ContentLen,
@@ -171,7 +170,7 @@ pub async fn edit(
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let intl = &rockets.intl.catalog; let intl = &rockets.intl.catalog;
let b = Blog::find_by_fqn(&rockets, &blog).await?; let b = Blog::find_by_fqn(&rockets, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
@@ -217,7 +216,7 @@ pub async fn edit(
} }
#[post("/~/<blog>/<slug>/edit", data = "<form>")] #[post("/~/<blog>/<slug>/edit", data = "<form>")]
pub async fn update( pub fn update(
blog: String, blog: String,
slug: String, slug: String,
cl: ContentLen, cl: ContentLen,
@@ -225,9 +224,7 @@ pub async fn update(
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog) let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error");
.await
.expect("post::update: blog error");
let mut post = let mut post =
Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error"); Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
@@ -301,19 +298,17 @@ pub async fn update(
post.license = form.license.clone(); post.license = form.license.clone();
post.cover_id = form.cover; post.cover_id = form.cover;
post.update(&*conn, &rockets.searcher) post.update(&*conn, &rockets.searcher)
.expect("post::update: update error"); .expect("post::update: update error");;
if post.published { if post.published {
// NOTE: here we unroll a filter_map(), so we can use .await painlessly post.update_mentions(
let mut filtered_mentions: Vec<link::Mention> = vec![]; &conn,
for m in mentions.into_iter() { mentions
match Mention::build_activity(&rockets, &m).await { .into_iter()
Ok(m) => filtered_mentions.push(m), .filter_map(|m| Mention::build_activity(&rockets, &m).ok())
Err(_) => {} .collect(),
} )
} .expect("post::update: mentions error");;
post.update_mentions(&conn, filtered_mentions)
.expect("post::update: mentions error");
} }
let tags = form let tags = form
@@ -326,7 +321,7 @@ pub async fn update(
.filter_map(|t| Tag::build_activity(t).ok()) .filter_map(|t| Tag::build_activity(t).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
post.update_tags(&conn, tags) post.update_tags(&conn, tags)
.expect(r#"post::update: tags error"#); .expect("post::update: tags error");
let hashtags = hashtags let hashtags = hashtags
.into_iter() .into_iter()
@@ -372,8 +367,8 @@ pub async fn update(
&*form, &*form,
form.draft, form.draft,
Some(post), Some(post),
errors, errors.clone(),
medias, medias.clone(),
cl.0 cl.0
)) ))
.into() .into()
@@ -404,16 +399,14 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/~/<blog_name>/new", data = "<form>")] #[post("/~/<blog_name>/new", data = "<form>")]
pub async fn create( pub fn create(
blog_name: String, blog_name: String,
form: LenientForm<NewPostForm>, form: LenientForm<NewPostForm>,
cl: ContentLen, cl: ContentLen,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> { ) -> Result<RespondOrRedirect, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &blog_name) let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");;
.await
.expect("post::create: blog error");
let slug = form.title.to_string().to_kebab_case(); let slug = form.title.to_string().to_kebab_case();
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
@@ -527,7 +520,6 @@ pub async fn create(
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&rockets, &m) &Mention::build_activity(&rockets, &m)
.await
.expect("post::create: mention build error"), .expect("post::create: mention build error"),
post.id, post.id,
true, true,
@@ -561,7 +553,7 @@ pub async fn create(
&*form, &*form,
form.draft, form.draft,
None, None,
errors, errors.clone(),
medias, medias,
cl.0 cl.0
)) ))
@@ -570,7 +562,7 @@ pub async fn create(
} }
#[post("/~/<blog_name>/<slug>/delete")] #[post("/~/<blog_name>/<slug>/delete")]
pub async fn delete( pub fn delete(
blog_name: String, blog_name: String,
slug: String, slug: String,
rockets: PlumeRocket, rockets: PlumeRocket,
@@ -578,7 +570,6 @@ pub async fn delete(
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
let post = Blog::find_by_fqn(&rockets, &blog_name) let post = Blog::find_by_fqn(&rockets, &blog_name)
.await
.and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id)); .and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id));
if let Ok(post) = post { if let Ok(post) = post {
@@ -588,7 +579,9 @@ pub async fn delete(
.any(|a| a.id == user.id) .any(|a| a.id == user.id)
{ {
return Ok(Flash::error( return Ok(Flash::error(
Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)), Redirect::to(
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
),
i18n!(intl.catalog, "You are not allowed to delete this article."), i18n!(intl.catalog, "You are not allowed to delete this article."),
)); ));
} }
@@ -624,13 +617,12 @@ pub async fn delete(
} }
#[get("/~/<blog_name>/<slug>/remote_interact")] #[get("/~/<blog_name>/<slug>/remote_interact")]
pub async fn remote_interact( pub fn remote_interact(
rockets: PlumeRocket, rockets: PlumeRocket,
blog_name: String, blog_name: String,
slug: String, slug: String,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let target = Blog::find_by_fqn(&rockets, &blog_name) let target = Blog::find_by_fqn(&rockets, &blog_name)
.await
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?; .and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
Ok(render!(posts::remote_interact( Ok(render!(posts::remote_interact(
&rockets.to_context(), &rockets.to_context(),
@@ -643,19 +635,17 @@ pub async fn remote_interact(
} }
#[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")] #[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")]
pub async fn remote_interact_post( pub fn remote_interact_post(
rockets: PlumeRocket, rockets: PlumeRocket,
blog_name: String, blog_name: String,
slug: String, slug: String,
remote: LenientForm<RemoteForm>, remote: LenientForm<RemoteForm>,
) -> Result<RespondOrRedirect, ErrorPage> { ) -> Result<RespondOrRedirect, ErrorPage> {
let target = Blog::find_by_fqn(&rockets, &blog_name) let target = Blog::find_by_fqn(&rockets, &blog_name)
.await
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?; .and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
if let uri = User::fetch_remote_interact_uri(&remote.remote) if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
.await .ok()
.map(|uri| uri.replace("{uri}", &format!("{}", target.ap_url))) .and_then(|uri| rt_format!(uri, uri = target.ap_url).ok())
.unwrap()
{ {
Ok(Redirect::to(uri).into()) Ok(Redirect::to(uri).into())
} else { } else {
+3 -3
View File
@@ -1,23 +1,23 @@
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use crate::routes::errors::ErrorPage;
use plume_common::activity_pub::broadcast; use plume_common::activity_pub::broadcast;
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blogs::Blog, inbox::inbox, posts::Post, reshares::*, timeline::*, users::User, Error, blogs::Blog, inbox::inbox, posts::Post, reshares::*, timeline::*, users::User, Error,
PlumeRocket, PlumeRocket,
}; };
use routes::errors::ErrorPage;
#[post("/~/<blog>/<slug>/reshare")] #[post("/~/<blog>/<slug>/reshare")]
pub async fn create( pub fn create(
blog: String, blog: String,
slug: String, slug: String,
user: User, user: User,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Redirect, ErrorPage> { ) -> Result<Redirect, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog).await?; let b = Blog::find_by_fqn(&rockets, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.has_reshared(&*conn, &post)? { if !user.has_reshared(&*conn, &post)? {
+2 -2
View File
@@ -1,10 +1,10 @@
use chrono::offset::Utc; use chrono::offset::Utc;
use rocket::request::Form; use rocket::request::Form;
use crate::routes::Page;
use crate::template_utils::{IntoContext, Ructe};
use plume_models::{search::Query, PlumeRocket}; use plume_models::{search::Query, PlumeRocket};
use routes::Page;
use std::str::FromStr; use std::str::FromStr;
use template_utils::{IntoContext, Ructe};
#[derive(Default, FromForm)] #[derive(Default, FromForm)]
pub struct SearchQuery { pub struct SearchQuery {
+15 -17
View File
@@ -1,13 +1,13 @@
use crate::routes::RespondOrRedirect;
use lettre::Transport; use lettre::Transport;
use rocket::http::ext::IntoOwned; use rocket::http::ext::IntoOwned;
use rocket::{ use rocket::{
http::{uri::Uri, Cookie, Cookies, SameSite}, http::{uri::Uri, Cookie, Cookies, SameSite},
request::LenientForm, request::{Form, LenientForm},
response::{Flash, Redirect}, response::{Flash, Redirect},
State, State,
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use routes::RespondOrRedirect;
use std::{ use std::{
borrow::Cow, borrow::Cow,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@@ -15,13 +15,13 @@ use std::{
}; };
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use crate::mail::{build_mail, Mailer}; use mail::{build_mail, Mailer};
use crate::template_utils::{IntoContext, Ructe};
use plume_models::{ use plume_models::{
password_reset_requests::*, password_reset_requests::*,
users::{User, AUTH_COOKIE}, users::{User, AUTH_COOKIE},
Error, PlumeRocket, CONFIG, Error, PlumeRocket, CONFIG,
}; };
use template_utils::{IntoContext, Ructe};
#[get("/login?<m>")] #[get("/login?<m>")]
pub fn new(m: Option<String>, rockets: PlumeRocket) -> Ructe { pub fn new(m: Option<String>, rockets: PlumeRocket) -> Ructe {
@@ -35,23 +35,21 @@ pub fn new(m: Option<String>, rockets: PlumeRocket) -> Ructe {
#[derive(Default, FromForm, Validate)] #[derive(Default, FromForm, Validate)]
pub struct LoginForm { pub struct LoginForm {
#[validate(length(min = 1, message = "We need an email, or a username to identify you"))] #[validate(length(min = "1", message = "We need an email, or a username to identify you"))]
pub email_or_name: String, pub email_or_name: String,
#[validate(length(min = 1, message = "Your password can't be empty"))] #[validate(length(min = "1", message = "Your password can't be empty"))]
pub password: String, pub password: String,
} }
#[post("/login", data = "<form>")] #[post("/login", data = "<form>")]
pub async fn create( pub fn create(
form: LenientForm<LoginForm>, form: LenientForm<LoginForm>,
mut cookies: Cookies<'_>, mut cookies: Cookies,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = match User::find_by_email(&*conn, &form.email_or_name) { let user = User::find_by_email(&*conn, &form.email_or_name)
Ok(user) => Ok(user), .or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
Err(_) => User::find_by_fqn(&rockets, &form.email_or_name).await,
};
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e, Err(e) => e,
@@ -120,7 +118,7 @@ pub async fn create(
} }
#[get("/logout")] #[get("/logout")]
pub fn delete(mut cookies: Cookies<'_>, intl: I18n) -> Flash<Redirect> { pub fn delete(mut cookies: Cookies, intl: I18n) -> Flash<Redirect> {
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie); cookies.remove_private(cookie);
} }
@@ -160,8 +158,8 @@ pub struct ResetForm {
#[post("/password-reset", data = "<form>")] #[post("/password-reset", data = "<form>")]
pub fn password_reset_request( pub fn password_reset_request(
mail: State<'_, Arc<Mutex<Mailer>>>, mail: State<Arc<Mutex<Mailer>>>,
form: LenientForm<ResetForm>, form: Form<ResetForm>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Ructe { ) -> Ructe {
if User::find_by_email(&*rockets.conn, &form.email).is_ok() { if User::find_by_email(&*rockets.conn, &form.email).is_ok() {
@@ -199,7 +197,7 @@ pub fn password_reset_form(token: String, rockets: PlumeRocket) -> Result<Ructe,
#[derive(FromForm, Default, Validate)] #[derive(FromForm, Default, Validate)]
#[validate(schema( #[validate(schema(
function = "passwords_match", function = "passwords_match",
skip_on_field_errors = false, skip_on_field_errors = "false",
message = "Passwords are not matching" message = "Passwords are not matching"
))] ))]
pub struct NewPasswordForm { pub struct NewPasswordForm {
@@ -218,7 +216,7 @@ fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> {
#[post("/password-reset/<token>", data = "<form>")] #[post("/password-reset/<token>", data = "<form>")]
pub fn password_reset( pub fn password_reset(
token: String, token: String,
form: LenientForm<NewPasswordForm>, form: Form<NewPasswordForm>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, Ructe> { ) -> Result<Flash<Redirect>, Ructe> {
form.validate() form.validate()
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::routes::{errors::ErrorPage, Page};
use crate::template_utils::{IntoContext, Ructe};
use plume_models::{posts::Post, PlumeRocket}; use plume_models::{posts::Post, PlumeRocket};
use routes::{errors::ErrorPage, Page};
use template_utils::{IntoContext, Ructe};
#[get("/tag/<name>?<page>")] #[get("/tag/<name>?<page>")]
pub fn tag(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn tag(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
+2 -2
View File
@@ -1,10 +1,10 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::routes::Page;
use crate::template_utils::IntoContext;
use crate::{routes::errors::ErrorPage, template_utils::Ructe}; use crate::{routes::errors::ErrorPage, template_utils::Ructe};
use plume_models::{timeline::*, PlumeRocket}; use plume_models::{timeline::*, PlumeRocket};
use rocket::response::Redirect; use rocket::response::Redirect;
use routes::Page;
use template_utils::IntoContext;
#[get("/timeline/<id>?<page>")] #[get("/timeline/<id>?<page>")]
pub fn details(id: i32, rockets: PlumeRocket, page: Option<Page>) -> Result<Ructe, ErrorPage> { pub fn details(id: i32, rockets: PlumeRocket, page: Option<Page>) -> Result<Ructe, ErrorPage> {
+79 -85
View File
@@ -2,6 +2,7 @@ use activitypub::{
activity::Create, activity::Create,
collection::{OrderedCollection, OrderedCollectionPage}, collection::{OrderedCollection, OrderedCollectionPage},
}; };
use atom_syndication::{Entry, FeedBuilder};
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
use rocket::{ use rocket::{
http::{ContentType, Cookies}, http::{ContentType, Cookies},
@@ -9,12 +10,11 @@ use rocket::{
response::{status, Content, Flash, Redirect}, response::{status, Content, Flash, Redirect},
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use serde_json;
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use crate::inbox; use inbox;
use crate::routes::{errors::ErrorPage, Page, RemoteForm, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, inbox::FromId, ActivityStream, ApRequest, Id}; use plume_common::activity_pub::{broadcast, inbox::FromId, ActivityStream, ApRequest, Id};
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
@@ -31,6 +31,8 @@ use plume_models::{
users::*, users::*,
Error, PlumeRocket, Error, PlumeRocket,
}; };
use routes::{errors::ErrorPage, Page, RemoteForm, RespondOrRedirect};
use template_utils::{IntoContext, Ructe};
#[get("/me")] #[get("/me")]
pub fn me(user: Option<User>) -> RespondOrRedirect { pub fn me(user: Option<User>) -> RespondOrRedirect {
@@ -41,7 +43,7 @@ pub fn me(user: Option<User>) -> RespondOrRedirect {
} }
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
pub async fn details( pub fn details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
fetch_rockets: PlumeRocket, fetch_rockets: PlumeRocket,
@@ -49,7 +51,7 @@ pub async fn details(
update_conn: DbConn, update_conn: DbConn,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name)?;
let recents = Post::get_recents_for_author(&*conn, &user, 6)?; let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?; let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
let worker = &rockets.worker; let worker = &rockets.worker;
@@ -60,7 +62,6 @@ pub async fn details(
worker.execute(move || { worker.execute(move || {
for create_act in user_clone for create_act in user_clone
.fetch_outbox::<Create>() .fetch_outbox::<Create>()
.await
.expect("Remote user: outbox couldn't be fetched") .expect("Remote user: outbox couldn't be fetched")
{ {
match create_act.create_props.object_object::<LicensedArticle>() { match create_act.create_props.object_object::<LicensedArticle>() {
@@ -79,7 +80,6 @@ pub async fn details(
worker.execute(move || { worker.execute(move || {
for user_id in user_clone for user_id in user_clone
.fetch_followers_ids() .fetch_followers_ids()
.await
.expect("Remote user: fetching followers error") .expect("Remote user: fetching followers error")
{ {
let follower = User::from_id(&fetch_followers_rockets, &user_id, None) let follower = User::from_id(&fetch_followers_rockets, &user_id, None)
@@ -102,7 +102,6 @@ pub async fn details(
worker.execute(move || { worker.execute(move || {
user_clone user_clone
.refetch(&*update_conn) .refetch(&*update_conn)
.await
.expect("Couldn't update user info"); .expect("Couldn't update user info");
}); });
} }
@@ -148,13 +147,13 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
} }
#[post("/@/<name>/follow")] #[post("/@/<name>/follow")]
pub async fn follow( pub fn follow(
name: String, name: String,
user: User, user: User,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let target = User::find_by_fqn(&rockets, &name).await?; let target = User::find_by_fqn(&rockets, &name)?;
let message = if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) { let message = if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.build_undo(&*conn)?; let delete_act = follow.build_undo(&*conn)?;
local_inbox( local_inbox(
@@ -192,26 +191,26 @@ pub async fn follow(
} }
#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)] #[post("/@/<name>/follow", data = "<remote_form>", rank = 2)]
pub async fn follow_not_connected( pub fn follow_not_connected(
rockets: PlumeRocket, rockets: PlumeRocket,
name: String, name: String,
remote_form: Option<LenientForm<RemoteForm>>, remote_form: Option<LenientForm<RemoteForm>>,
i18n: I18n, i18n: I18n,
) -> Result<RespondOrRedirect, ErrorPage> { ) -> Result<RespondOrRedirect, ErrorPage> {
let target = User::find_by_fqn(&rockets, &name).await?; let target = User::find_by_fqn(&rockets, &name)?;
if let Some(remote_form) = remote_form { if let Some(remote_form) = remote_form {
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form) if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
.await
.ok() .ok()
.and_then(|uri| { .and_then(|uri| {
Some(uri.replace( rt_format!(
"{uri}", uri,
&format!( uri = format!(
"{}@{}", "{}@{}",
target.fqn, target.fqn,
target.get_instance(&rockets.conn).ok()?.public_domain target.get_instance(&rockets.conn).ok()?.public_domain
), )
)) )
.ok()
}) })
{ {
Ok(Redirect::to(uri).into()) Ok(Redirect::to(uri).into())
@@ -268,18 +267,15 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/@/<name>/followers?<page>", rank = 2)] #[get("/@/<name>/followers?<page>", rank = 2)]
pub async fn followers( pub fn followers(
name: String, name: String,
page: Option<Page>, page: Option<Page>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user: User = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name)?;
let followers_count = match user.count_followers(&conn) { let followers_count = user.count_followers(&*conn)?;
Ok(num) => num,
Err(_) => 0,
};
Ok(render!(users::followers( Ok(render!(users::followers(
&rockets.to_context(), &rockets.to_context(),
@@ -298,14 +294,14 @@ pub async fn followers(
} }
#[get("/@/<name>/followed?<page>", rank = 2)] #[get("/@/<name>/followed?<page>", rank = 2)]
pub async fn followed( pub fn followed(
name: String, name: String,
page: Option<Page>, page: Option<Page>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name)?;
let followed_count = user.count_followed(conn)?; let followed_count = user.count_followed(conn)?;
Ok(render!(users::followed( Ok(render!(users::followed(
@@ -325,12 +321,12 @@ pub async fn followed(
} }
#[get("/@/<name>", rank = 1)] #[get("/@/<name>", rank = 1)]
pub async fn activity_details( pub fn activity_details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<CustomPerson>> { ) -> Option<ActivityStream<CustomPerson>> {
let user: User = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name).ok()?;
Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?)) Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?))
} }
@@ -359,7 +355,7 @@ pub fn edit(name: String, user: User, rockets: PlumeRocket) -> Result<Ructe, Err
ValidationErrors::default() ValidationErrors::default()
))) )))
} else { } else {
Err(Error::Unauthorized.into()) Err(Error::Unauthorized)?
} }
} }
@@ -403,10 +399,7 @@ pub fn update(
) )
.0, .0,
); );
user.preferred_theme = form user.preferred_theme = form.theme.clone();
.theme
.clone()
.and_then(|t| if &t == "" { None } else { Some(t) });
user.hide_custom_css = form.hide_custom_css; user.hide_custom_css = form.hide_custom_css;
let _: User = user.save_changes(&*conn).map_err(Error::from)?; let _: User = user.save_changes(&*conn).map_err(Error::from)?;
@@ -417,49 +410,50 @@ pub fn update(
} }
#[post("/@/<name>/delete")] #[post("/@/<name>/delete")]
pub async fn delete( pub fn delete(
name: String, name: String,
user: User, user: User,
mut cookies: Cookies<'_>, mut cookies: Cookies,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
let account = User::find_by_fqn(&rockets, &name).await?; let account = User::find_by_fqn(&rockets, &name)?;
if user.id != account.id { if user.id == account.id {
return Ok(Flash::error( account.delete(&*rockets.conn, &rockets.searcher)?;
let target = User::one_by_instance(&*rockets.conn)?;
let delete_act = account.delete_activity(&*rockets.conn)?;
rockets
.worker
.execute(move || broadcast(&account, delete_act, target));
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie);
}
Ok(Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your account has been deleted."),
))
} else {
Ok(Flash::error(
Redirect::to(uri!(edit: name = name)), Redirect::to(uri!(edit: name = name)),
i18n!( i18n!(
rockets.intl.catalog, rockets.intl.catalog,
"You can't delete someone else's account." "You can't delete someone else's account."
), ),
)); ))
} }
account.delete(&*rockets.conn, &rockets.searcher)?;
let target = User::one_by_instance(&*rockets.conn)?;
let delete_act = account.delete_activity(&*rockets.conn)?;
rockets
.worker
.execute(move || broadcast(&account, delete_act, target));
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie);
}
Ok(Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your account has been deleted."),
))
} }
#[derive(Default, FromForm, Validate)] #[derive(Default, FromForm, Validate)]
#[validate(schema( #[validate(schema(
function = "passwords_match", function = "passwords_match",
skip_on_field_errors = false, skip_on_field_errors = "false",
message = "Passwords are not matching" message = "Passwords are not matching"
))] ))]
pub struct NewUserForm { pub struct NewUserForm {
#[validate( #[validate(
length(min = 1, message = "Username can't be empty"), length(min = "1", message = "Username can't be empty"),
custom( custom(
function = "validate_username", function = "validate_username",
message = "User name is not allowed to contain any of < > & @ ' or \"" message = "User name is not allowed to contain any of < > & @ ' or \""
@@ -468,9 +462,9 @@ pub struct NewUserForm {
pub username: String, pub username: String,
#[validate(email(message = "Invalid email"))] #[validate(email(message = "Invalid email"))]
pub email: String, pub email: String,
#[validate(length(min = 8, message = "Password should be at least 8 characters long"))] #[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
pub password: String, pub password: String,
#[validate(length(min = 8, message = "Password should be at least 8 characters long"))] #[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
pub password_confirmation: String, pub password_confirmation: String,
} }
@@ -569,44 +563,37 @@ pub fn create(
} }
#[get("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
pub async fn outbox( pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> {
name: String, let user = User::find_by_fqn(&rockets, &name).ok()?;
rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_by_fqn(&rockets, &name).await.ok()?;
user.outbox(&*rockets.conn).ok() user.outbox(&*rockets.conn).ok()
} }
#[get("/@/<name>/outbox?<page>")] #[get("/@/<name>/outbox?<page>")]
pub async fn outbox_page( pub fn outbox_page(
name: String, name: String,
page: Page, page: Page,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollectionPage>> { ) -> Option<ActivityStream<OrderedCollectionPage>> {
let user = User::find_by_fqn(&rockets, &name).await.ok()?; let user = User::find_by_fqn(&rockets, &name).ok()?;
user.outbox_page(&*rockets.conn, page.limits()).ok() user.outbox_page(&*rockets.conn, page.limits()).ok()
} }
#[post("/@/<name>/inbox", data = "<data>")] #[post("/@/<name>/inbox", data = "<data>")]
pub async fn inbox( pub fn inbox(
name: String, name: String,
data: inbox::SignedJson<serde_json::Value>, data: inbox::SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<String, status::BadRequest<&'static str>> { ) -> Result<String, status::BadRequest<&'static str>> {
User::find_by_fqn(&rockets, &name) User::find_by_fqn(&rockets, &name).map_err(|_| status::BadRequest(Some("User not found")))?;
.await inbox::handle_incoming(rockets, data, headers)
.map_err(|_| status::BadRequest(Some("User not found")))?;
inbox::handle_incoming(rockets, data, headers).await
} }
#[get("/@/<name>/followers", rank = 1)] #[get("/@/<name>/followers", rank = 1)]
pub async fn ap_followers( pub fn ap_followers(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<OrderedCollection>> { ) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name).ok()?;
let followers = user let followers = user
.get_followers(&*rockets.conn) .get_followers(&*rockets.conn)
.ok()? .ok()?
@@ -626,16 +613,23 @@ pub async fn ap_followers(
} }
#[get("/@/<name>/atom.xml")] #[get("/@/<name>/atom.xml")]
pub async fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> { pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let author = User::find_by_fqn(&rockets, &name).await?; let author = User::find_by_fqn(&rockets, &name).ok()?;
let entries = Post::get_recents_for_author(conn, &author, 15).ok()?; let feed = FeedBuilder::default()
let uri = Instance::get_local() .title(author.display_name.clone())
.ok()? .id(Instance::get_local()
.compute_box("@", &name, "atom.xml"); .unwrap()
let title = &author.display_name; .compute_box("@", &name, "atom.xml"))
let default_updated = &author.creation_date; .entries(
let feed = super::build_atom_feed(entries, &uri, title, default_updated, conn); Post::get_recents_for_author(conn, &author, 15)
.ok()?
.into_iter()
.map(|p| super::post_to_atom(p, conn))
.collect::<Vec<Entry>>(),
)
.build()
.expect("user::atom_feed: Error building Atom feed");
Some(Content( Some(Content(
ContentType::new("application", "atom+xml"), ContentType::new("application", "atom+xml"),
feed.to_string(), feed.to_string(),
+6 -36
View File
@@ -1,5 +1,6 @@
use rocket::http::ContentType; use rocket::http::ContentType;
use rocket::response::Content; use rocket::response::Content;
use serde_json;
use webfinger::*; use webfinger::*;
use plume_models::{ap_url, blogs::Blog, users::User, PlumeRocket, CONFIG}; use plume_models::{ap_url, blogs::Blog, users::User, PlumeRocket, CONFIG};
@@ -42,58 +43,27 @@ pub fn host_meta() -> String {
struct WebfingerResolver; struct WebfingerResolver;
#[async_trait::async_trait] impl Resolver<PlumeRocket> for WebfingerResolver {
impl AsyncResolver for WebfingerResolver { fn instance_domain<'a>() -> &'a str {
type Repo = PlumeRocket;
async fn instance_domain<'a>(&self) -> &'a str {
CONFIG.base_url.as_str() CONFIG.base_url.as_str()
} }
async fn find( fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
&self,
prefix: Prefix,
acct: String,
ctx: PlumeRocket,
) -> Result<Webfinger, ResolverError> {
match prefix { match prefix {
Prefix::Acct => User::find_by_fqn(&ctx, &acct) Prefix::Acct => User::find_by_fqn(&ctx, &acct)
.await
.and_then(|usr| usr.webfinger(&*ctx.conn)) .and_then(|usr| usr.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound)), .or(Err(ResolverError::NotFound)),
Prefix::Group => Blog::find_by_fqn(&ctx, &acct) Prefix::Group => Blog::find_by_fqn(&ctx, &acct)
.await
.and_then(|blog| blog.webfinger(&*ctx.conn)) .and_then(|blog| blog.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound)), .or(Err(ResolverError::NotFound)),
Prefix::Custom(_) => Err(ResolverError::NotFound), Prefix::Custom(_) => Err(ResolverError::NotFound),
} }
} }
async fn endpoint<R: Into<String> + Send>(
&self,
resource: R,
resource_repo: PlumeRocket,
) -> Result<Webfinger, ResolverError> {
let resource = resource.into();
let mut parsed_query = resource.splitn(2, ':');
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
let mut parsed_res = res.splitn(2, '@');
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
if domain == self.instance_domain().await {
self.find(res_prefix, user.to_string(), resource_repo).await
} else {
Err(ResolverError::WrongDomain)
}
}
} }
#[get("/.well-known/webfinger?<resource>")] #[get("/.well-known/webfinger?<resource>")]
pub async fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> { pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
let wf_resolver = WebfingerResolver; match WebfingerResolver::endpoint(resource, rockets)
match wf_resolver
.endpoint(resource, rockets)
.await
.and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) .and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound))
{ {
Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf), Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf),
+10 -10
View File
@@ -1,12 +1,13 @@
use plume_models::{notifications::*, users::User, Connection, PlumeRocket}; use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
use crate::templates::Html; use rocket::http::hyper::header::{ETag, EntityTag};
use rocket::http::{Header, Method, Status}; use rocket::http::{Method, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::{self, content::Html as HtmlCt, Responder, Response}; use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog; use rocket_i18n::Catalog;
use std::collections::{btree_map::BTreeMap, hash_map::DefaultHasher}; use std::collections::{btree_map::BTreeMap, hash_map::DefaultHasher};
use std::hash::Hasher; use std::hash::Hasher;
use templates::Html;
pub use askama_escape::escape; pub use askama_escape::escape;
@@ -51,12 +52,11 @@ impl IntoContext for PlumeRocket {
#[derive(Debug)] #[derive(Debug)]
pub struct Ructe(pub Vec<u8>); pub struct Ructe(pub Vec<u8>);
#[rocket::async_trait]
impl<'r> Responder<'r> for Ructe { impl<'r> Responder<'r> for Ructe {
async fn respond_to(self, r: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, r: &Request) -> response::Result<'r> {
//if method is not Get or page contain a form, no caching //if method is not Get or page contain a form, no caching
if r.method() != Method::Get || self.0.windows(6).any(|w| w == b"<form ") { if r.method() != Method::Get || self.0.windows(6).any(|w| w == b"<form ") {
return HtmlCt(self.0).respond_to(r).await; return HtmlCt(self.0).respond_to(r);
} }
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
hasher.write(&self.0); hasher.write(&self.0);
@@ -67,12 +67,12 @@ impl<'r> Responder<'r> for Ructe {
{ {
Response::build() Response::build()
.status(Status::NotModified) .status(Status::NotModified)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} else { } else {
Response::build() Response::build()
.merge(HtmlCt(self.0).respond_to(r).await.ok().unwrap()) .merge(HtmlCt(self.0).respond_to(r)?)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} }
} }
@@ -82,7 +82,7 @@ impl<'r> Responder<'r> for Ructe {
macro_rules! render { macro_rules! render {
($group:tt :: $page:tt ( $( $param:expr ),* ) ) => { ($group:tt :: $page:tt ( $( $param:expr ),* ) ) => {
{ {
use crate::templates; use templates;
let mut res = vec![]; let mut res = vec![];
templates::$group::$page( templates::$group::$page(
@@ -96,7 +96,7 @@ macro_rules! render {
} }
} }
pub fn translate_notification(ctx: BaseContext<'_>, notif: Notification) -> String { pub fn translate_notification(ctx: BaseContext, notif: Notification) -> String {
let name = notif.get_actor(ctx.0).unwrap().name(); let name = notif.get_actor(ctx.0).unwrap().name();
match notif.kind.as_ref() { match notif.kind.as_ref() {
notification_kind::COMMENT => i18n!(ctx.1, "{0} commented on your article."; &name), notification_kind::COMMENT => i18n!(ctx.1, "{0} commented on your article."; &name),
+2 -7
View File
@@ -1,13 +1,8 @@
@use plume_models::CONFIG; @use plume_models::CONFIG;
@use plume_models::instance::Instance; @use plume_models::instance::Instance;
@use rocket::http::RawStr; @use template_utils::*;
@use rocket::request::Form; @use routes::*;
@use std::path::Path; @use std::path::Path;
@use std::path::PathBuf;
@use crate::template_utils::*;
@use crate::routes::*;
@use crate::routes::search::SearchQuery;
@(ctx: BaseContext, title: String, head: Content, header: Content, content: Content) @(ctx: BaseContext, title: String, head: Content, header: Content, content: Content)
<!DOCTYPE html> <!DOCTYPE html>
+3 -5
View File
@@ -2,12 +2,10 @@
@use plume_models::instance::Instance; @use plume_models::instance::Instance;
@use plume_models::posts::Post; @use plume_models::posts::Post;
@use plume_models::users::User; @use plume_models::users::User;
@use rocket::http::RawStr; @use templates::{base, partials::post_card};
@use template_utils::*;
@use routes::*;
@use std::path::Path; @use std::path::Path;
@use std::path::PathBuf;
@use crate::templates::{base, partials::post_card};
@use crate::template_utils::*;
@use crate::routes::*;
@(ctx: BaseContext, blog: Blog, authors: &[User], page: i32, n_pages: i32, posts: Vec<Post>) @(ctx: BaseContext, blog: Blog, authors: &[User], page: i32, n_pages: i32, posts: Vec<Post>)
+6 -8
View File
@@ -2,14 +2,12 @@
@use plume_models::blogs::Blog; @use plume_models::blogs::Blog;
@use plume_models::instance::Instance; @use plume_models::instance::Instance;
@use plume_models::medias::Media; @use plume_models::medias::Media;
@use crate::template_utils::*; @use routes::blogs;
@use crate::templates::base; @use routes::blogs::EditForm;
@use crate::templates::partials::image_select; @use routes::medias;
@use crate::routes::Page; @use template_utils::*;
@use crate::routes::blogs; @use templates::base;
@use crate::routes::blogs::EditForm; @use templates::partials::image_select;
@use crate::routes::medias;
@(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors) @(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors)
+4 -4
View File
@@ -1,8 +1,8 @@
@use validator::ValidationErrors; @use validator::ValidationErrors;
@use crate::templates::base; @use templates::base;
@use crate::template_utils::*; @use template_utils::*;
@use crate::routes::blogs::NewBlogForm; @use routes::blogs::NewBlogForm;
@use crate::routes::*; @use routes::*;
@(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors) @(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors)
+2 -2
View File
@@ -1,5 +1,5 @@
@use crate::templates::base as base_template; @use templates::base as base_template;
@use crate::template_utils::*; @use template_utils::*;
@(ctx: BaseContext, error_message: String, error: Content) @(ctx: BaseContext, error_message: String, error: Content)
+2 -2
View File
@@ -1,5 +1,5 @@
@use crate::templates::errors::base; @use templates::errors::base;
@use crate::template_utils::*; @use template_utils::*;
@(ctx: BaseContext) @(ctx: BaseContext)
+2 -2
View File
@@ -1,5 +1,5 @@
@use crate::templates::errors::base; @use templates::errors::base;
@use crate::template_utils::*; @use template_utils::*;
@(ctx: BaseContext, error_message: String) @(ctx: BaseContext, error_message: String)
+3 -2
View File
@@ -1,5 +1,5 @@
@use crate::templates::errors::base; @use templates::errors::base;
@use crate::template_utils::*; @use template_utils::*;
@(ctx: BaseContext) @(ctx: BaseContext)
@@ -7,3 +7,4 @@
<h1>@i18n!(ctx.1, "We couldn't find this page.")</h1> <h1>@i18n!(ctx.1, "We couldn't find this page.")</h1>
<p>@i18n!(ctx.1, "The link that led you here may be broken.")</p> <p>@i18n!(ctx.1, "The link that led you here may be broken.")</p>
}) })
+2 -2
View File
@@ -1,5 +1,5 @@
@use crate::templates::errors::base; @use templates::errors::base;
@use crate::template_utils::*; @use template_utils::*;
@(ctx: BaseContext) @(ctx: BaseContext)
@@ -1,5 +1,5 @@
@use crate::templates::errors::base; @use templates::errors::base;
@use crate::template_utils::*; @use template_utils::*;
@(ctx: BaseContext) @(ctx: BaseContext)
@@ -7,3 +7,4 @@
<h1>@i18n!(ctx.1, "The content you sent can't be processed.")</h1> <h1>@i18n!(ctx.1, "The content you sent can't be processed.")</h1>
<p>@i18n!(ctx.1, "Maybe it was too long.")</p> <p>@i18n!(ctx.1, "Maybe it was too long.")</p>
}) })
+3 -3
View File
@@ -1,7 +1,7 @@
@use plume_models::{instance::Instance, users::User}; @use plume_models::{instance::Instance, users::User};
@use crate::templates::base; @use templates::base;
@use crate::template_utils::*; @use template_utils::*;
@use crate::routes::*; @use routes::*;
@(ctx: BaseContext, instance: Instance, admin: User, n_users: i64, n_articles: i64, n_instances: i64) @(ctx: BaseContext, instance: Instance, admin: User, n_users: i64, n_articles: i64, n_instances: i64)

Some files were not shown because too many files have changed in this diff Show More