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