Add unit tests for main model parts (#310)

Add tests for following models:
- Blog
- Instance
- Media
- User
This commit is contained in:
fdb-hiroshima 2018-11-24 12:44:17 +01:00 committed by Baptiste Gelez
parent 0b9727ed28
commit 8a4702df92
30 changed files with 3779 additions and 1123 deletions

View file

@ -39,7 +39,7 @@ jobs:
name: "Test with potgresql backend" name: "Test with potgresql backend"
env: env:
- MIGRATION_DIR=migrations/postgres FEATURES=postgres DATABASE_URL=postgres://postgres@localhost/plume_tests - MIGRATION_DIR=migrations/postgres FEATURES=postgres DATABASE_URL=postgres://postgres@localhost/plume_tests
- RUSTFLAGS='-C link-dead-code' - RUSTFLAGS='-C link-dead-code' RUST_TEST_THREADS=1
before_script: psql -c 'create database plume_tests;' -U postgres before_script: psql -c 'create database plume_tests;' -U postgres
script: script:
- | - |
@ -49,7 +49,7 @@ jobs:
name: "Test with Sqlite backend" name: "Test with Sqlite backend"
env: env:
- MIGRATION_DIR=migrations/sqlite FEATURES=sqlite DATABASE_URL=plume.sqlite3 - MIGRATION_DIR=migrations/sqlite FEATURES=sqlite DATABASE_URL=plume.sqlite3
- RUSTFLAGS='-C link-dead-code' - RUSTFLAGS='-C link-dead-code' RUST_TEST_THREADS=1
script: script:
- | - |
cargo test --features "${FEATURES}" --no-default-features --all && cargo test --features "${FEATURES}" --no-default-features --all &&

View file

@ -1,4 +1,4 @@
use activitypub::{Object, activity::Create}; use activitypub::{activity::Create, Object};
use activity_pub::Id; use activity_pub::Id;
@ -9,7 +9,7 @@ pub enum InboxError {
#[fail(display = "Invalid activity type")] #[fail(display = "Invalid activity type")]
InvalidType, InvalidType,
#[fail(display = "Couldn't undo activity")] #[fail(display = "Couldn't undo activity")]
CantUndo CantUndo,
} }
pub trait FromActivity<T: Object, C>: Sized { pub trait FromActivity<T: Object, C>: Sized {
@ -17,7 +17,13 @@ pub trait FromActivity<T: Object, C>: Sized {
fn try_from_activity(conn: &C, act: Create) -> bool { fn try_from_activity(conn: &C, act: Create) -> bool {
if let Ok(obj) = act.create_props.object_object() { if let Ok(obj) = act.create_props.object_object() {
Self::from_activity(conn, obj, act.create_props.actor_link::<Id>().expect("FromActivity::try_from_activity: id not found error")); Self::from_activity(
conn,
obj,
act.create_props
.actor_link::<Id>()
.expect("FromActivity::try_from_activity: id not found error"),
);
true true
} else { } else {
false false
@ -32,7 +38,6 @@ pub trait Notify<C> {
pub trait Deletable<C, A> { pub trait Deletable<C, A> {
fn delete(&self, conn: &C) -> A; fn delete(&self, conn: &C) -> A;
fn delete_id(id: String, actor_id: String, conn: &C); fn delete_id(id: String, actor_id: String, conn: &C);
} }
pub trait WithInbox { pub trait WithInbox {

View file

@ -1,10 +1,11 @@
use activitypub::{Activity, Actor, Object, Link}; use activitypub::{Activity, Actor, Link, Object};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use reqwest::Client; use reqwest::Client;
use rocket::{ use rocket::{
Outcome, http::Status, http::Status,
response::{Response, Responder}, request::{FromRequest, Request},
request::{FromRequest, Request} response::{Responder, Response},
Outcome,
}; };
use serde_json; use serde_json;
@ -24,7 +25,7 @@ pub fn ap_accept_header() -> Vec<&'static str> {
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"", "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"", "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
"application/activity+json", "application/activity+json",
"application/ld+json" "application/ld+json",
] ]
} }
@ -52,7 +53,7 @@ pub fn context() -> serde_json::Value {
]) ])
} }
pub struct ActivityStream<T> (T); pub struct ActivityStream<T>(T);
impl<T> ActivityStream<T> { impl<T> ActivityStream<T> {
pub fn new(t: T) -> ActivityStream<T> { pub fn new(t: T) -> ActivityStream<T> {
@ -64,9 +65,11 @@ impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> { fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context(); json["@context"] = context();
serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r) serde_json::to_string(&json).respond_to(request).map(|r| {
.raw_header("Content-Type", "application/activity+json") Response::build_from(r)
.finalize()) .raw_header("Content-Type", "application/activity+json")
.finalize()
})
} }
} }
@ -76,29 +79,45 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> { fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
request.headers().get_one("Accept").map(|header| header.split(",").map(|ct| match ct.trim() { request
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise .headers()
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" | .get_one("Accept")
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" | .map(|header| {
"application/activity+json" | header
"application/ld+json" => Outcome::Success(ApRequest), .split(",")
"text/html" => Outcome::Forward(true), .map(|ct| match ct.trim() {
_ => Outcome::Forward(false) // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
}).fold(Outcome::Forward(false), |out, ct| if out.clone().forwarded().unwrap_or(out.is_success()) { "application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
out | "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
} else { | "application/activity+json"
ct | "application/ld+json" => Outcome::Success(ApRequest),
}).map_forward(|_| ())).unwrap_or(Outcome::Forward(())) "text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false),
})
.fold(Outcome::Forward(false), |out, ct| {
if out.clone().forwarded().unwrap_or(out.is_success()) {
out
} else {
ct
}
})
.map_forward(|_| ())
})
.unwrap_or(Outcome::Forward(()))
} }
} }
pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(sender: &S, act: A, to: Vec<T>) { pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(
let boxes = to.into_iter() sender: &S,
act: A,
to: Vec<T>,
) {
let boxes = to
.into_iter()
.filter(|u| !u.is_local()) .filter(|u| !u.is_local())
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url())) .map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.unique(); .unique();
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error"); let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
act["@context"] = context(); act["@context"] = context();
let signed = act.sign(sender); let signed = act.sign(sender);
@ -121,8 +140,8 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(send
} else { } else {
println!("Error while reading response") println!("Error while reading response")
} }
}, }
Err(e) => println!("Error while sending to inbox ({:?})", e) Err(e) => println!("Error while sending to inbox ({:?})", e),
} }
} }
} }
@ -152,7 +171,7 @@ impl Link for Id {}
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApSignature { pub struct ApSignature {
#[activitystreams(concrete(PublicKey), functional)] #[activitystreams(concrete(PublicKey), functional)]
pub public_key: Option<serde_json::Value> pub public_key: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
@ -165,7 +184,7 @@ pub struct PublicKey {
pub owner: Option<serde_json::Value>, pub owner: Option<serde_json::Value>,
#[activitystreams(concrete(String), functional)] #[activitystreams(concrete(String), functional)]
pub public_key_pem: Option<serde_json::Value> pub public_key_pem: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, UnitString)] #[derive(Clone, Debug, Default, UnitString)]

View file

@ -1,5 +1,5 @@
use base64; use base64;
use chrono::{DateTime, offset::Utc}; use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT}; use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT};
use std::ops::Deref; use std::ops::Deref;
@ -14,35 +14,52 @@ pub struct Digest(String);
impl Digest { impl Digest {
pub fn digest(body: String) -> HeaderValue { pub fn digest(body: String) -> HeaderValue {
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); let mut hasher =
hasher.update(&body.into_bytes()[..]).expect("Digest::digest: content insertion error"); Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
hasher
.update(&body.into_bytes()[..])
.expect("Digest::digest: content insertion error");
let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error")); let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error"));
HeaderValue::from_str(&format!("SHA-256={}", res)).expect("Digest::digest: header creation error") HeaderValue::from_str(&format!("SHA-256={}", res))
.expect("Digest::digest: header creation error")
} }
pub fn verify(&self, body: String) -> bool { pub fn verify(&self, body: String) -> bool {
if self.algorithm()=="SHA-256" { if self.algorithm() == "SHA-256" {
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); let mut hasher =
hasher.update(&body.into_bytes()).expect("Digest::digest: content insertion error"); Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
self.value().deref()==hasher.finish().expect("Digest::digest: finalizing error").deref() hasher
.update(&body.into_bytes())
.expect("Digest::digest: content insertion error");
self.value().deref()
== hasher
.finish()
.expect("Digest::digest: finalizing error")
.deref()
} else { } else {
false //algorithm not supported false //algorithm not supported
} }
} }
pub fn algorithm(&self) -> &str { pub fn algorithm(&self) -> &str {
let pos = self.0.find('=').expect("Digest::algorithm: invalid header error"); let pos = self
.0
.find('=')
.expect("Digest::algorithm: invalid header error");
&self.0[..pos] &self.0[..pos]
} }
pub fn value(&self) -> Vec<u8> { pub fn value(&self) -> Vec<u8> {
let pos = self.0.find('=').expect("Digest::value: invalid header error")+1; let pos = self
.0
.find('=')
.expect("Digest::value: invalid header error") + 1;
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error") base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
} }
pub fn from_header(dig: &str) -> Result<Self, ()> { pub fn from_header(dig: &str) -> Result<Self, ()> {
if let Some(pos) = dig.find('=') { if let Some(pos) = dig.find('=') {
let pos = pos+1; let pos = pos + 1;
if let Ok(_) = base64::decode(&dig[pos..]) { if let Ok(_) = base64::decode(&dig[pos..]) {
Ok(Digest(dig.to_owned())) Ok(Digest(dig.to_owned()))
} else { } else {
@ -60,15 +77,42 @@ pub fn headers() -> HeaderMap {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT)); headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT));
headers.insert(DATE, HeaderValue::from_str(&date).expect("request::headers: date error")); headers.insert(
headers.insert(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::<Vec<_>>().join(", ")).expect("request::headers: accept error")); DATE,
HeaderValue::from_str(&date).expect("request::headers: date error"),
);
headers.insert(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
).expect("request::headers: accept error"),
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE)); headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
headers headers
} }
pub fn signature<S: Signer>(signer: &S, headers: HeaderMap) -> HeaderValue { pub fn signature<S: Signer>(signer: &S, headers: HeaderMap) -> HeaderValue {
let signed_string = headers.iter().map(|(h,v)| format!("{}: {}", h.as_str().to_lowercase(), v.to_str().expect("request::signature: invalid header error"))).collect::<Vec<String>>().join("\n"); let signed_string = headers
let signed_headers = headers.iter().map(|(h,_)| h.as_str()).collect::<Vec<&str>>().join(" ").to_lowercase(); .iter()
.map(|(h, v)| {
format!(
"{}: {}",
h.as_str().to_lowercase(),
v.to_str()
.expect("request::signature: invalid header error")
)
})
.collect::<Vec<String>>()
.join("\n");
let signed_headers = headers
.iter()
.map(|(h, _)| h.as_str())
.collect::<Vec<&str>>()
.join(" ")
.to_lowercase();
let data = signer.sign(signed_string); let data = signer.sign(signed_string);
let sign = base64::encode(&data[..]); let sign = base64::encode(&data[..]);

View file

@ -1,12 +1,8 @@
use super::request;
use base64; use base64;
use chrono::Utc; use chrono::Utc;
use hex; use hex;
use openssl::{ use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
pkey::PKey,
rsa::Rsa,
sha::sha256
};
use super::request;
use rocket::http::HeaderMap; use rocket::http::HeaderMap;
use serde_json; use serde_json;
@ -15,8 +11,12 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
let keypair = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error"); let keypair = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error");
( (
keypair.public_key_to_pem().expect("sign::gen_keypair: public key encoding error"), keypair
keypair.private_key_to_pem_pkcs8().expect("sign::gen_keypair: private key encoding error") .public_key_to_pem()
.expect("sign::gen_keypair: public key encoding error"),
keypair
.private_key_to_pem_pkcs8()
.expect("sign::gen_keypair: private key encoding error"),
) )
} }
@ -30,8 +30,12 @@ pub trait Signer {
} }
pub trait Signable { pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer; fn sign<T>(&mut self, creator: &T) -> &mut Self
fn verify<T>(self, creator: &T) -> bool where T: Signer; where
T: Signer;
fn verify<T>(self, creator: &T) -> bool
where
T: Signer;
fn hash(data: String) -> String { fn hash(data: String) -> String {
let bytes = data.into_bytes(); let bytes = data.into_bytes();
@ -48,10 +52,12 @@ impl Signable for serde_json::Value {
"created": creation_date "created": creation_date
}); });
let options_hash = Self::hash(json!({ let options_hash = Self::hash(
json!({
"@context": "https://w3id.org/identity/v1", "@context": "https://w3id.org/identity/v1",
"created": creation_date "created": creation_date
}).to_string()); }).to_string(),
);
let document_hash = Self::hash(self.to_string()); let document_hash = Self::hash(self.to_string());
let to_be_signed = options_hash + &document_hash; let to_be_signed = options_hash + &document_hash;
@ -63,29 +69,34 @@ impl Signable for serde_json::Value {
} }
fn verify<T: Signer>(mut self, creator: &T) -> bool { fn verify<T: Signer>(mut self, creator: &T) -> bool {
let signature_obj = if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) { let signature_obj =
if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) {
sig
} else {
//signature not present
return false;
};
let signature = if let Ok(sig) =
base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or(""))
{
sig sig
} else { } else {
//signature not present return false;
return false
};
let signature = if let Ok(sig) = base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) {
sig
} else {
return false
}; };
let creation_date = &signature_obj["created"]; let creation_date = &signature_obj["created"];
let options_hash = Self::hash(json!({ let options_hash = Self::hash(
json!({
"@context": "https://w3id.org/identity/v1", "@context": "https://w3id.org/identity/v1",
"created": creation_date "created": creation_date
}).to_string()); }).to_string(),
);
let document_hash = Self::hash(self.to_string()); let document_hash = Self::hash(self.to_string());
let to_be_signed = options_hash + &document_hash; let to_be_signed = options_hash + &document_hash;
creator.verify(to_be_signed, signature) creator.verify(to_be_signed, signature)
} }
} }
#[derive(Debug,Copy,Clone,PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum SignatureValidity { pub enum SignatureValidity {
Invalid, Invalid,
ValidNoDigest, ValidNoDigest,
@ -95,14 +106,18 @@ pub enum SignatureValidity {
impl SignatureValidity { impl SignatureValidity {
pub fn is_secure(&self) -> bool { pub fn is_secure(&self) -> bool {
self==&SignatureValidity::Valid self == &SignatureValidity::Valid
} }
} }
pub fn verify_http_headers<S: Signer+::std::fmt::Debug>(sender: &S, all_headers: HeaderMap, data: String) -> SignatureValidity{ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
sender: &S,
all_headers: HeaderMap,
data: String,
) -> SignatureValidity {
let sig_header = all_headers.get_one("Signature"); let sig_header = all_headers.get_one("Signature");
if sig_header.is_none() { if sig_header.is_none() {
return SignatureValidity::Absent return SignatureValidity::Absent;
} }
let sig_header = sig_header.expect("sign::verify_http_headers: unreachable"); let sig_header = sig_header.expect("sign::verify_http_headers: unreachable");
@ -112,35 +127,43 @@ pub fn verify_http_headers<S: Signer+::std::fmt::Debug>(sender: &S, all_headers:
let mut signature = None; let mut signature = None;
for part in sig_header.split(',') { for part in sig_header.split(',') {
match part { match part {
part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len()-1]), part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len() - 1]),
part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len()-1]), part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len() - 1]),
part if part.starts_with("headers=") => headers = Some(&part[9..part.len()-1]), part if part.starts_with("headers=") => headers = Some(&part[9..part.len() - 1]),
part if part.starts_with("signature=") => signature = Some(&part[11..part.len()-1]), part if part.starts_with("signature=") => signature = Some(&part[11..part.len() - 1]),
_ => {}, _ => {}
} }
} }
if signature.is_none() || headers.is_none() {//missing part of the header if signature.is_none() || headers.is_none() {
return SignatureValidity::Invalid //missing part of the header
return SignatureValidity::Invalid;
} }
let headers = headers.expect("sign::verify_http_headers: unreachable").split_whitespace().collect::<Vec<_>>(); let headers = headers
.expect("sign::verify_http_headers: unreachable")
.split_whitespace()
.collect::<Vec<_>>();
let signature = signature.expect("sign::verify_http_headers: unreachable"); let signature = signature.expect("sign::verify_http_headers: unreachable");
let h = headers.iter() let h = headers
.map(|header| (header,all_headers.get_one(header))) .iter()
.map(|header| (header, all_headers.get_one(header)))
.map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or(""))) .map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or("")))
.collect::<Vec<_>>().join("\n"); .collect::<Vec<_>>()
.join("\n");
if !sender.verify(h, base64::decode(signature).unwrap_or(Vec::new())) { if !sender.verify(h, base64::decode(signature).unwrap_or(Vec::new())) {
return SignatureValidity::Invalid return SignatureValidity::Invalid;
} }
if !headers.contains(&"digest") {// signature is valid, but body content is not verified if !headers.contains(&"digest") {
return SignatureValidity::ValidNoDigest // signature is valid, but body content is not verified
return SignatureValidity::ValidNoDigest;
} }
let digest = all_headers.get_one("digest").unwrap_or(""); let digest = all_headers.get_one("digest").unwrap_or("");
let digest = request::Digest::from_header(digest); let digest = request::Digest::from_header(digest);
if !digest.map(|d| d.verify(data)).unwrap_or(false) {// signature was valid, but body content does not match its digest if !digest.map(|d| d.verify(data)).unwrap_or(false) {
// signature was valid, but body content does not match its digest
SignatureValidity::Invalid SignatureValidity::Invalid
} else { } else {
SignatureValidity::Valid// all check passed SignatureValidity::Valid // all check passed
} }
} }

View file

@ -1,4 +1,8 @@
use rocket::{Outcome, http::Status, request::{self, FromRequest, Request}}; use rocket::{
http::Status,
request::{self, FromRequest, Request},
Outcome,
};
use users::User; use users::User;

View file

@ -1,9 +1,9 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::{ use rocket::{
Outcome,
http::Status, http::Status,
request::{self, FromRequest, Request} request::{self, FromRequest, Request},
Outcome,
}; };
use db_conn::DbConn; use db_conn::DbConn;
@ -48,7 +48,7 @@ impl ApiToken {
let full_scope = what.to_owned() + ":" + scope; let full_scope = what.to_owned() + ":" + scope;
for s in self.scopes.split('+') { for s in self.scopes.split('+') {
if s == what || s == full_scope { if s == what || s == full_scope {
return true return true;
} }
} }
false false

23
plume-models/src/apps.rs Executable file → Normal file
View file

@ -1,11 +1,11 @@
use canapi::{Error, Provider}; use canapi::{Error, Provider};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_api::apps::AppEndpoint; use plume_api::apps::AppEndpoint;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use Connection;
use schema::apps; use schema::apps;
use Connection;
#[derive(Clone, Queryable)] #[derive(Clone, Queryable)]
pub struct App { pub struct App {
@ -19,7 +19,7 @@ pub struct App {
} }
#[derive(Insertable)] #[derive(Insertable)]
#[table_name= "apps"] #[table_name = "apps"]
pub struct NewApp { pub struct NewApp {
pub name: String, pub name: String,
pub client_id: String, pub client_id: String,
@ -43,13 +43,16 @@ impl Provider<Connection> for App {
let client_id = random_hex(); let client_id = random_hex();
let client_secret = random_hex(); let client_secret = random_hex();
let app = App::insert(conn, NewApp { let app = App::insert(
name: data.name, conn,
client_id: client_id, NewApp {
client_secret: client_secret, name: data.name,
redirect_uri: data.redirect_uri, client_id: client_id,
website: data.website, client_secret: client_secret,
}); redirect_uri: data.redirect_uri,
website: data.website,
},
);
Ok(AppEndpoint { Ok(AppEndpoint {
id: Some(app.id), id: Some(app.id),

View file

@ -1,4 +1,4 @@
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::blog_authors; use schema::blog_authors;

View file

@ -1,30 +1,31 @@
use activitypub::{Actor, Object, CustomObject, actor::Group, collection::OrderedCollection}; use activitypub::{actor::Group, collection::OrderedCollection, Actor, CustomObject, Object};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use reqwest::{Client, use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
header::{ACCEPT, HeaderValue}
};
use serde_json;
use url::Url;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
pkey::{PKey, Private}, pkey::{PKey, Private},
rsa::Rsa, rsa::Rsa,
sign::{Signer,Verifier} sign::{Signer, Verifier},
}; };
use reqwest::{
header::{HeaderValue, ACCEPT},
Client,
};
use serde_json;
use url::Url;
use webfinger::*; use webfinger::*;
use {BASE_URL, USE_HTTPS, Connection};
use plume_common::activity_pub::{
ap_accept_header, ApSignature, ActivityStream, Id, IntoId, PublicKey,
inbox::{Deletable, WithInbox},
sign
};
use safe_string::SafeString;
use instance::*; use instance::*;
use plume_common::activity_pub::{
ap_accept_header,
inbox::{Deletable, WithInbox},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey,
};
use posts::Post; use posts::Post;
use safe_string::SafeString;
use schema::blogs; use schema::blogs;
use users::User; use users::User;
use {Connection, BASE_URL, USE_HTTPS};
pub type CustomGroup = CustomObject<ApSignature, Group>; pub type CustomGroup = CustomObject<ApSignature, Group>;
@ -40,7 +41,7 @@ pub struct Blog {
pub creation_date: NaiveDateTime, pub creation_date: NaiveDateTime,
pub ap_url: String, pub ap_url: String,
pub private_key: Option<String>, pub private_key: Option<String>,
pub public_key: String pub public_key: String,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -54,7 +55,7 @@ pub struct NewBlog {
pub instance_id: i32, pub instance_id: i32,
pub ap_url: String, pub ap_url: String,
pub private_key: Option<String>, pub private_key: Option<String>,
pub public_key: String pub public_key: String,
} }
const BLOG_PREFIX: &'static str = "~"; const BLOG_PREFIX: &'static str = "~";
@ -72,16 +73,22 @@ impl Blog {
pub fn list_authors(&self, conn: &Connection) -> Vec<User> { pub fn list_authors(&self, conn: &Connection) -> Vec<User> {
use schema::blog_authors; use schema::blog_authors;
use schema::users; use schema::users;
let authors_ids = blog_authors::table.filter(blog_authors::blog_id.eq(self.id)).select(blog_authors::author_id); let authors_ids = blog_authors::table
users::table.filter(users::id.eq_any(authors_ids)) .filter(blog_authors::blog_id.eq(self.id))
.select(blog_authors::author_id);
users::table
.filter(users::id.eq_any(authors_ids))
.load::<User>(conn) .load::<User>(conn)
.expect("Blog::list_authors: author loading error") .expect("Blog::list_authors: author loading error")
} }
pub fn find_for_author(conn: &Connection, author_id: i32) -> Vec<Blog> { pub fn find_for_author(conn: &Connection, author: &User) -> Vec<Blog> {
use schema::blog_authors; use schema::blog_authors;
let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id); let author_ids = blog_authors::table
blogs::table.filter(blogs::id.eq_any(author_ids)) .filter(blog_authors::author_id.eq(author.id))
.select(blog_authors::blog_id);
blogs::table
.filter(blogs::id.eq_any(author_ids))
.load::<Blog>(conn) .load::<Blog>(conn)
.expect("Blog::find_for_author: blog loading error") .expect("Blog::find_for_author: blog loading error")
} }
@ -91,24 +98,49 @@ impl Blog {
} }
pub fn find_by_fqn(conn: &Connection, fqn: String) -> Option<Blog> { pub fn find_by_fqn(conn: &Connection, fqn: String) -> Option<Blog> {
if fqn.contains("@") { // remote blog if fqn.contains("@") {
match Instance::find_by_domain(conn, String::from(fqn.split("@").last().expect("Blog::find_by_fqn: unreachable"))) { // remote blog
Some(instance) => { match Instance::find_by_domain(
match Blog::find_by_name(conn, String::from(fqn.split("@").nth(0).expect("Blog::find_by_fqn: unreachable")), instance.id) { conn,
Some(u) => Some(u), String::from(
None => Blog::fetch_from_webfinger(conn, fqn) fqn.split("@")
} .last()
.expect("Blog::find_by_fqn: unreachable"),
),
) {
Some(instance) => match Blog::find_by_name(
conn,
String::from(
fqn.split("@")
.nth(0)
.expect("Blog::find_by_fqn: unreachable"),
),
instance.id,
) {
Some(u) => Some(u),
None => Blog::fetch_from_webfinger(conn, fqn),
}, },
None => Blog::fetch_from_webfinger(conn, fqn) None => Blog::fetch_from_webfinger(conn, fqn),
} }
} else { // local blog } else {
// local blog
Blog::find_local(conn, fqn) Blog::find_local(conn, fqn)
} }
} }
fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option<Blog> { fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option<Blog> {
match resolve(acct.clone(), *USE_HTTPS) { match resolve(acct.clone(), *USE_HTTPS) {
Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| Blog::fetch_from_url(conn, l.href.expect("Blog::fetch_from_webfinger: href not found error"))), Ok(wf) => wf
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.and_then(|l| {
Blog::fetch_from_url(
conn,
l.href
.expect("Blog::fetch_from_webfinger: href not found error"),
)
}),
Err(details) => { Err(details) => {
println!("{:?}", details); println!("{:?}", details);
None None
@ -119,17 +151,37 @@ impl Blog {
fn fetch_from_url(conn: &Connection, url: String) -> Option<Blog> { fn fetch_from_url(conn: &Connection, url: String) -> Option<Blog> {
let req = Client::new() let req = Client::new()
.get(&url[..]) .get(&url[..])
.header(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::<Vec<_>>().join(", ")).expect("Blog::fetch_from_url: accept_header generation error")) .header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
).expect("Blog::fetch_from_url: accept_header generation error"),
)
.send(); .send();
match req { match req {
Ok(mut res) => { Ok(mut res) => {
let text = &res.text().expect("Blog::fetch_from_url: body reading error"); let text = &res
let ap_sign: ApSignature = serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); .text()
let mut json: CustomGroup = serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); .expect("Blog::fetch_from_url: body reading error");
let ap_sign: ApSignature =
serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error");
let mut json: CustomGroup =
serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error");
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
Some(Blog::from_activity(conn, json, Url::parse(url.as_ref()).expect("Blog::fetch_from_url: url parsing error").host_str().expect("Blog::fetch_from_url: host extraction error").to_string())) Some(Blog::from_activity(
}, conn,
Err(_) => None json,
Url::parse(url.as_ref())
.expect("Blog::fetch_from_url: url parsing error")
.host_str()
.expect("Blog::fetch_from_url: host extraction error")
.to_string(),
))
}
Err(_) => None,
} }
} }
@ -137,49 +189,103 @@ impl Blog {
let instance = match Instance::find_by_domain(conn, inst.clone()) { let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
None => { None => {
Instance::insert(conn, NewInstance { Instance::insert(
public_domain: inst.clone(), conn,
name: inst.clone(), NewInstance {
local: false, public_domain: inst.clone(),
// We don't really care about all the following for remote instances name: inst.clone(),
long_description: SafeString::new(""), local: false,
short_description: SafeString::new(""), // We don't really care about all the following for remote instances
default_license: String::new(), long_description: SafeString::new(""),
open_registrations: true, short_description: SafeString::new(""),
short_description_html: String::new(), default_license: String::new(),
long_description_html: String::new() open_registrations: true,
}) short_description_html: String::new(),
long_description_html: String::new(),
},
)
} }
}; };
Blog::insert(conn, NewBlog { Blog::insert(
actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"), conn,
title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"), NewBlog {
outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"), actor_id: acct
inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"), .object
summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"), .ap_actor_props
instance_id: instance.id, .preferred_username_string()
ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"), .expect("Blog::from_activity: preferredUsername error"),
public_key: acct.custom_props.public_key_publickey().expect("Blog::from_activity: publicKey error") title: acct
.public_key_pem_string().expect("Blog::from_activity: publicKey.publicKeyPem error"), .object
private_key: None .object_props
}) .name_string()
.expect("Blog::from_activity: name error"),
outbox_url: acct
.object
.ap_actor_props
.outbox_string()
.expect("Blog::from_activity: outbox error"),
inbox_url: acct
.object
.ap_actor_props
.inbox_string()
.expect("Blog::from_activity: inbox error"),
summary: acct
.object
.object_props
.summary_string()
.expect("Blog::from_activity: summary error"),
instance_id: instance.id,
ap_url: acct
.object
.object_props
.id_string()
.expect("Blog::from_activity: id error"),
public_key: acct
.custom_props
.public_key_publickey()
.expect("Blog::from_activity: publicKey error")
.public_key_pem_string()
.expect("Blog::from_activity: publicKey.publicKeyPem error"),
private_key: None,
},
)
} }
pub fn into_activity(&self, _conn: &Connection) -> CustomGroup { pub fn into_activity(&self, _conn: &Connection) -> CustomGroup {
let mut blog = Group::default(); let mut blog = Group::default();
blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error"); blog.ap_actor_props
blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error"); .set_preferred_username_string(self.actor_id.clone())
blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error"); .expect("Blog::into_activity: preferredUsername error");
blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error"); blog.object_props
blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error"); .set_name_string(self.title.clone())
blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error"); .expect("Blog::into_activity: name error");
blog.ap_actor_props
.set_outbox_string(self.outbox_url.clone())
.expect("Blog::into_activity: outbox error");
blog.ap_actor_props
.set_inbox_string(self.inbox_url.clone())
.expect("Blog::into_activity: inbox error");
blog.object_props
.set_summary_string(self.summary.clone())
.expect("Blog::into_activity: summary error");
blog.object_props
.set_id_string(self.ap_url.clone())
.expect("Blog::into_activity: id error");
let mut public_key = PublicKey::default(); let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error"); public_key
public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error"); .set_id_string(format!("{}#main-key", self.ap_url))
public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error"); .expect("Blog::into_activity: publicKey.id error");
public_key
.set_owner_string(self.ap_url.clone())
.expect("Blog::into_activity: publicKey.owner error");
public_key
.set_public_key_pem_string(self.public_key.clone())
.expect("Blog::into_activity: publicKey.publicKeyPem error");
let mut ap_signature = ApSignature::default(); let mut ap_signature = ApSignature::default();
ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error"); ap_signature
.set_public_key_publickey(public_key)
.expect("Blog::into_activity: publicKey error");
CustomGroup::new(blog, ap_signature) CustomGroup::new(blog, ap_signature)
} }
@ -188,27 +294,41 @@ impl Blog {
let instance = self.get_instance(conn); let instance = self.get_instance(conn);
if self.outbox_url.len() == 0 { if self.outbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox"))) .set(blogs::outbox_url.eq(instance.compute_box(
.execute(conn).expect("Blog::update_boxes: outbox update error"); BLOG_PREFIX,
self.actor_id.clone(),
"outbox",
)))
.execute(conn)
.expect("Blog::update_boxes: outbox update error");
} }
if self.inbox_url.len() == 0 { if self.inbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox"))) .set(blogs::inbox_url.eq(instance.compute_box(
.execute(conn).expect("Blog::update_boxes: inbox update error"); BLOG_PREFIX,
self.actor_id.clone(),
"inbox",
)))
.execute(conn)
.expect("Blog::update_boxes: inbox update error");
} }
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), ""))) .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "")))
.execute(conn).expect("Blog::update_boxes: ap_url update error"); .execute(conn)
.expect("Blog::update_boxes: ap_url update error");
} }
} }
pub fn outbox(&self, conn: &Connection) -> ActivityStream<OrderedCollection> { pub fn outbox(&self, conn: &Connection) -> ActivityStream<OrderedCollection> {
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)).expect("Blog::outbox: activity serialization error"); coll.collection_props.items = serde_json::to_value(self.get_activities(conn))
coll.collection_props.set_total_items_u64(self.get_activities(conn).len() as u64).expect("Blog::outbox: count serialization error"); .expect("Blog::outbox: activity serialization error");
coll.collection_props
.set_total_items_u64(self.get_activities(conn).len() as u64)
.expect("Blog::outbox: count serialization error");
ActivityStream::new(coll) ActivityStream::new(coll)
} }
@ -217,35 +337,48 @@ impl Blog {
} }
pub fn get_keypair(&self) -> PKey<Private> { pub fn get_keypair(&self) -> PKey<Private> {
PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().expect("Blog::get_keypair: private key not found error").as_ref()) PKey::from_rsa(
.expect("Blog::get_keypair: pem parsing error")) Rsa::private_key_from_pem(
.expect("Blog::get_keypair: private key deserialization error") self.private_key
.clone()
.expect("Blog::get_keypair: private key not found error")
.as_ref(),
).expect("Blog::get_keypair: pem parsing error"),
).expect("Blog::get_keypair: private key deserialization error")
} }
pub fn webfinger(&self, conn: &Connection) -> Webfinger { pub fn webfinger(&self, conn: &Connection) -> Webfinger {
Webfinger { Webfinger {
subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain), subject: format!(
"acct:{}@{}",
self.actor_id,
self.get_instance(conn).public_domain
),
aliases: vec![self.ap_url.clone()], aliases: vec![self.ap_url.clone()],
links: vec![ links: vec![
Link { Link {
rel: String::from("http://webfinger.net/rel/profile-page"), rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None, mime_type: None,
href: Some(self.ap_url.clone()), href: Some(self.ap_url.clone()),
template: None template: None,
}, },
Link { Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"), rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")), mime_type: Some(String::from("application/atom+xml")),
href: Some(self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")), href: Some(self.get_instance(conn).compute_box(
template: None BLOG_PREFIX,
self.actor_id.clone(),
"feed.atom",
)),
template: None,
}, },
Link { Link {
rel: String::from("self"), rel: String::from("self"),
mime_type: Some(String::from("application/activity+json")), mime_type: Some(String::from("application/activity+json")),
href: Some(self.ap_url.clone()), href: Some(self.ap_url.clone()),
template: None template: None,
} },
] ],
} }
} }
@ -253,7 +386,11 @@ impl Blog {
Blog::find_by_ap_url(conn, url.clone()).or_else(|| { Blog::find_by_ap_url(conn, url.clone()).or_else(|| {
// The requested blog was not in the DB // The requested blog was not in the DB
// We try to fetch it if it is remote // We try to fetch it if it is remote
if Url::parse(url.as_ref()).expect("Blog::from_url: ap_url parsing error").host_str().expect("Blog::from_url: host extraction error") != BASE_URL.as_str() { if Url::parse(url.as_ref())
.expect("Blog::from_url: ap_url parsing error")
.host_str()
.expect("Blog::from_url: host extraction error") != BASE_URL.as_str()
{
Blog::fetch_from_url(conn, url) Blog::fetch_from_url(conn, url)
} else { } else {
None None
@ -265,7 +402,11 @@ impl Blog {
if self.instance_id == Instance::local_id(conn) { if self.instance_id == Instance::local_id(conn) {
self.actor_id.clone() self.actor_id.clone()
} else { } else {
format!("{}@{}", self.actor_id, self.get_instance(conn).public_domain) format!(
"{}@{}",
self.actor_id,
self.get_instance(conn).public_domain
)
} }
} }
@ -277,9 +418,11 @@ impl Blog {
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) {
for post in Post::get_for_blog(conn, &self) { for post in Post::get_for_blog(conn, &self) {
post.delete(conn); post.delete(conn);
} }
diesel::delete(self).execute(conn).expect("Blog::delete: blog deletion error"); diesel::delete(self)
.execute(conn)
.expect("Blog::delete: blog deletion error");
} }
} }
@ -313,17 +456,29 @@ impl sign::Signer for Blog {
fn sign(&self, to_sign: String) -> Vec<u8> { fn sign(&self, to_sign: String) -> Vec<u8> {
let key = self.get_keypair(); let key = self.get_keypair();
let mut signer = Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error"); let mut signer =
signer.update(to_sign.as_bytes()).expect("Blog::sign: content insertion error"); Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error");
signer.sign_to_vec().expect("Blog::sign: finalization error") signer
.update(to_sign.as_bytes())
.expect("Blog::sign: content insertion error");
signer
.sign_to_vec()
.expect("Blog::sign: finalization error")
} }
fn verify(&self, data: String, signature: Vec<u8>) -> bool { fn verify(&self, data: String, signature: Vec<u8>) -> bool {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).expect("Blog::verify: pem parsing error")) let key = PKey::from_rsa(
.expect("Blog::verify: deserialization error"); Rsa::public_key_from_pem(self.public_key.as_ref())
let mut verifier = Verifier::new(MessageDigest::sha256(), &key).expect("Blog::verify: initialization error"); .expect("Blog::verify: pem parsing error"),
verifier.update(data.as_bytes()).expect("Blog::verify: content insertion error"); ).expect("Blog::verify: deserialization error");
verifier.verify(&signature).expect("Blog::verify: finalization error") let mut verifier = Verifier::new(MessageDigest::sha256(), &key)
.expect("Blog::verify: initialization error");
verifier
.update(data.as_bytes())
.expect("Blog::verify: content insertion error");
verifier
.verify(&signature)
.expect("Blog::verify: finalization error")
} }
} }
@ -332,7 +487,7 @@ impl NewBlog {
actor_id: String, actor_id: String,
title: String, title: String,
summary: String, summary: String,
instance_id: i32 instance_id: i32,
) -> NewBlog { ) -> NewBlog {
let (pub_key, priv_key) = sign::gen_keypair(); let (pub_key, priv_key) = sign::gen_keypair();
NewBlog { NewBlog {
@ -344,7 +499,337 @@ impl NewBlog {
instance_id: instance_id, instance_id: instance_id,
ap_url: String::from(""), ap_url: String::from(""),
public_key: String::from_utf8(pub_key).expect("NewBlog::new_local: public key error"), public_key: String::from_utf8(pub_key).expect("NewBlog::new_local: public key error"),
private_key: Some(String::from_utf8(priv_key).expect("NewBlog::new_local: private key error")) private_key: Some(
String::from_utf8(priv_key).expect("NewBlog::new_local: private key error"),
),
} }
} }
} }
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use blog_authors::*;
use diesel::Connection;
use instance::tests as instance_tests;
use tests::db;
use users::tests as usersTests;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<Blog> {
instance_tests::fill_database(conn);
let users = usersTests::fill_database(conn);
let blogs = vec![
NewBlog::new_local(
"BlogName".to_owned(),
"Blog name".to_owned(),
"This is a small blog".to_owned(),
Instance::local_id(conn),
),
NewBlog::new_local(
"MyBlog".to_owned(),
"My blog".to_owned(),
"Welcome to my blog".to_owned(),
Instance::local_id(conn),
),
NewBlog::new_local(
"WhyILikePlume".to_owned(),
"Why I like Plume".to_owned(),
"In this blog I will explay you why I like Plume so much".to_owned(),
Instance::local_id(conn),
),
].into_iter()
.map(|nb| Blog::insert(conn, nb))
.collect::<Vec<_>>();
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blogs[0].id,
author_id: users[0].id,
is_owner: true,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blogs[0].id,
author_id: users[1].id,
is_owner: false,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blogs[1].id,
author_id: users[1].id,
is_owner: true,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blogs[2].id,
author_id: users[2].id,
is_owner: true,
},
);
blogs
}
#[test]
fn get_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
let blog = Blog::insert(
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::local_id(conn),
),
);
assert_eq!(blog.get_instance(conn).id, Instance::local_id(conn));
// TODO add tests for remote instance
Ok(())
});
}
#[test]
fn authors() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let user = usersTests::fill_database(conn);
fill_database(conn);
let blog = vec![
Blog::insert(
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::local_id(conn),
),
),
Blog::insert(
conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
"I've named my blog Blog".to_owned(),
Instance::local_id(conn),
),
),
];
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
is_owner: true,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
is_owner: false,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
is_owner: true,
},
);
assert!(
blog[0]
.list_authors(conn)
.iter()
.any(|a| a.id == user[0].id)
);
assert!(
blog[0]
.list_authors(conn)
.iter()
.any(|a| a.id == user[1].id)
);
assert!(
blog[1]
.list_authors(conn)
.iter()
.any(|a| a.id == user[0].id)
);
assert!(
!blog[1]
.list_authors(conn)
.iter()
.any(|a| a.id == user[1].id)
);
assert!(
Blog::find_for_author(conn, &user[0])
.iter()
.any(|b| b.id == blog[0].id)
);
assert!(
Blog::find_for_author(conn, &user[1])
.iter()
.any(|b| b.id == blog[0].id)
);
assert!(
Blog::find_for_author(conn, &user[0])
.iter()
.any(|b| b.id == blog[1].id)
);
assert!(
!Blog::find_for_author(conn, &user[1])
.iter()
.any(|b| b.id == blog[1].id)
);
Ok(())
});
}
#[test]
fn find_local() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
let blog = Blog::insert(
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::local_id(conn),
),
);
assert_eq!(
Blog::find_local(conn, "SomeName".to_owned()).unwrap().id,
blog.id
);
Ok(())
});
}
#[test]
fn get_fqn() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
let blog = Blog::insert(
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::local_id(conn),
),
);
assert_eq!(blog.get_fqn(conn), "SomeName");
Ok(())
});
}
#[test]
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let blogs = fill_database(conn);
blogs[0].delete(conn);
assert!(Blog::get(conn, blogs[0].id).is_none());
Ok(())
});
}
#[test]
fn delete_via_user() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let user = usersTests::fill_database(conn);
fill_database(conn);
let blog = vec![
Blog::insert(
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::local_id(conn),
),
),
Blog::insert(
conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
"I've named my blog Blog".to_owned(),
Instance::local_id(conn),
),
),
];
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
is_owner: true,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
is_owner: false,
},
);
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
is_owner: true,
},
);
user[0].delete(conn);
assert!(Blog::get(conn, blog[0].id).is_some());
assert!(Blog::get(conn, blog[1].id).is_none());
user[1].delete(conn);
assert!(Blog::get(conn, blog[0].id).is_none());
Ok(())
});
}
}

View file

@ -1,25 +1,21 @@
use activitypub::{ use activitypub::{activity::Create, link, object::Note};
activity::Create,
link,
object::{Note}
};
use chrono::{self, NaiveDateTime}; use chrono::{self, NaiveDateTime};
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use serde_json; use serde_json;
use plume_common::activity_pub::{
Id, IntoId, PUBLIC_VISIBILTY,
inbox::{FromActivity, Notify}
};
use plume_common::utils;
use Connection;
use instance::Instance; use instance::Instance;
use mentions::Mention; use mentions::Mention;
use notifications::*; use notifications::*;
use plume_common::activity_pub::{
inbox::{FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY,
};
use plume_common::utils;
use posts::Post; use posts::Post;
use users::User;
use schema::comments;
use safe_string::SafeString; use safe_string::SafeString;
use schema::comments;
use users::User;
use Connection;
#[derive(Queryable, Identifiable, Serialize, Clone)] #[derive(Queryable, Identifiable, Serialize, Clone)]
pub struct Comment { pub struct Comment {
@ -31,7 +27,7 @@ pub struct Comment {
pub creation_date: NaiveDateTime, pub creation_date: NaiveDateTime,
pub ap_url: Option<String>, pub ap_url: Option<String>,
pub sensitive: bool, pub sensitive: bool,
pub spoiler_text: String pub spoiler_text: String,
} }
#[derive(Insertable, Default)] #[derive(Insertable, Default)]
@ -43,7 +39,7 @@ pub struct NewComment {
pub author_id: i32, pub author_id: i32,
pub ap_url: Option<String>, pub ap_url: Option<String>,
pub sensitive: bool, pub sensitive: bool,
pub spoiler_text: String pub spoiler_text: String,
} }
impl Comment { impl Comment {
@ -62,24 +58,35 @@ impl Comment {
pub fn count_local(conn: &Connection) -> usize { pub fn count_local(conn: &Connection) -> usize {
use schema::users; use schema::users;
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); let local_authors = users::table
comments::table.filter(comments::author_id.eq_any(local_authors)) .filter(users::instance_id.eq(Instance::local_id(conn)))
.select(users::id);
comments::table
.filter(comments::author_id.eq_any(local_authors))
.load::<Comment>(conn) .load::<Comment>(conn)
.expect("Comment::count_local: loading error") .expect("Comment::count_local: loading error")
.len()// TODO count in database? .len() // TODO count in database?
} }
pub fn to_json(&self, conn: &Connection, others: &Vec<Comment>) -> serde_json::Value { pub fn to_json(&self, conn: &Connection, others: &Vec<Comment>) -> serde_json::Value {
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error"); let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error");
json["author"] = self.get_author(conn).to_json(conn); json["author"] = self.get_author(conn).to_json(conn);
let mentions = Mention::list_for_comment(conn, self.id).into_iter() let mentions = Mention::list_for_comment(conn, self.id)
.map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new())) .into_iter()
.map(|m| {
m.get_mentioned(conn)
.map(|u| u.get_fqn(conn))
.unwrap_or(String::new())
})
.collect::<Vec<String>>(); .collect::<Vec<String>>();
json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error"); json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error");
json["responses"] = json!(others.into_iter() json["responses"] = json!(
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false)) others
.map(|c| c.to_json(conn, others)) .into_iter()
.collect::<Vec<_>>()); .filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
.map(|c| c.to_json(conn, others))
.collect::<Vec<_>>()
);
json json
} }
@ -106,61 +113,138 @@ impl Comment {
let mut note = Note::default(); let mut note = Note::default();
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())]; let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
note.object_props.set_id_string(self.ap_url.clone().unwrap_or(String::new())).expect("Comment::into_activity: id error"); note.object_props
note.object_props.set_summary_string(self.spoiler_text.clone()).expect("Comment::into_activity: summary error"); .set_id_string(self.ap_url.clone().unwrap_or(String::new()))
note.object_props.set_content_string(html).expect("Comment::into_activity: content error"); .expect("Comment::into_activity: id error");
note.object_props.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(|| Post::get(conn, self.post_id).expect("Comment::into_activity: post error").ap_url, |id| { note.object_props
let comm = Comment::get(conn, id).expect("Comment::into_activity: comment error"); .set_summary_string(self.spoiler_text.clone())
comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) .expect("Comment::into_activity: summary error");
}))).expect("Comment::into_activity: in_reply_to error"); note.object_props
note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("Comment::into_activity: published error"); .set_content_string(html)
note.object_props.set_attributed_to_link(author.clone().into_id()).expect("Comment::into_activity: attributed_to error"); .expect("Comment::into_activity: content error");
note.object_props.set_to_link_vec(to.clone()).expect("Comment::into_activity: to error"); note.object_props
note.object_props.set_tag_link_vec(mentions.into_iter().map(|m| Mention::build_activity(conn, m)).collect::<Vec<link::Mention>>()) .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|| {
Post::get(conn, self.post_id)
.expect("Comment::into_activity: post error")
.ap_url
},
|id| {
let comm =
Comment::get(conn, id).expect("Comment::into_activity: comment error");
comm.ap_url.clone().unwrap_or(comm.compute_id(conn))
},
)))
.expect("Comment::into_activity: in_reply_to error");
note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339())
.expect("Comment::into_activity: published error");
note.object_props
.set_attributed_to_link(author.clone().into_id())
.expect("Comment::into_activity: attributed_to error");
note.object_props
.set_to_link_vec(to.clone())
.expect("Comment::into_activity: to error");
note.object_props
.set_tag_link_vec(
mentions
.into_iter()
.map(|m| Mention::build_activity(conn, m))
.collect::<Vec<link::Mention>>(),
)
.expect("Comment::into_activity: tag error"); .expect("Comment::into_activity: tag error");
note note
} }
pub fn create_activity(&self, conn: &Connection) -> Create { pub fn create_activity(&self, conn: &Connection) -> Create {
let author = User::get(conn, self.author_id).expect("Comment::create_activity: author error"); let author =
User::get(conn, self.author_id).expect("Comment::create_activity: author error");
let note = self.into_activity(conn); let note = self.into_activity(conn);
let mut act = Create::default(); let mut act = Create::default();
act.create_props.set_actor_link(author.into_id()).expect("Comment::create_activity: actor error"); act.create_props
act.create_props.set_object_object(note.clone()).expect("Comment::create_activity: object error"); .set_actor_link(author.into_id())
act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().expect("Comment::create_activity: ap_url error"))).expect("Comment::create_activity: id error"); .expect("Comment::create_activity: actor error");
act.object_props.set_to_link_vec(note.object_props.to_link_vec::<Id>().expect("Comment::create_activity: id error")).expect("Comment::create_activity: to error"); act.create_props
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Comment::create_activity: cc error"); .set_object_object(note.clone())
.expect("Comment::create_activity: object error");
act.object_props
.set_id_string(format!(
"{}/activity",
self.ap_url
.clone()
.expect("Comment::create_activity: ap_url error")
))
.expect("Comment::create_activity: id error");
act.object_props
.set_to_link_vec(
note.object_props
.to_link_vec::<Id>()
.expect("Comment::create_activity: id error"),
)
.expect("Comment::create_activity: to error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Comment::create_activity: cc error");
act act
} }
} }
impl FromActivity<Note, Connection> for Comment { impl FromActivity<Note, Connection> for Comment {
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment { fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment {
let previous_url = note.object_props.in_reply_to.clone().expect("Comment::from_activity: not an answer error").as_str().expect("Comment::from_activity: in_reply_to parsing error").to_string(); let previous_url = note
.object_props
.in_reply_to
.clone()
.expect("Comment::from_activity: not an answer error")
.as_str()
.expect("Comment::from_activity: in_reply_to parsing error")
.to_string();
let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone()); let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone());
let comm = Comment::insert(conn, NewComment { let comm = Comment::insert(
content: SafeString::new(&note.object_props.content_string().expect("Comment::from_activity: content deserialization error")), conn,
spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), NewComment {
ap_url: note.object_props.id_string().ok(), content: SafeString::new(
in_response_to_id: previous_comment.clone().map(|c| c.id), &note
post_id: previous_comment .object_props
.map(|c| c.post_id) .content_string()
.unwrap_or_else(|| Post::find_by_ap_url(conn, previous_url).expect("Comment::from_activity: post error").id), .expect("Comment::from_activity: content deserialization error"),
author_id: User::from_url(conn, actor.clone().into()).expect("Comment::from_activity: author error").id, ),
sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate spoiler_text: note
}); .object_props
.summary_string()
.unwrap_or(String::from("")),
ap_url: note.object_props.id_string().ok(),
in_response_to_id: previous_comment.clone().map(|c| c.id),
post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| {
Post::find_by_ap_url(conn, previous_url)
.expect("Comment::from_activity: post error")
.id
}),
author_id: User::from_url(conn, actor.clone().into())
.expect("Comment::from_activity: author error")
.id,
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
},
);
// save mentions // save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags.into_iter() { for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag) serde_json::from_value::<link::Mention>(tag)
.map(|m| { .map(|m| {
let author = &Post::get(conn, comm.post_id).expect("Comment::from_activity: error").get_authors(conn)[0]; let author = &Post::get(conn, comm.post_id)
let not_author = m.link_props.href_string().expect("Comment::from_activity: no href error") != author.ap_url.clone(); .expect("Comment::from_activity: error")
.get_authors(conn)[0];
let not_author = m
.link_props
.href_string()
.expect("Comment::from_activity: no href error")
!= author.ap_url.clone();
Mention::from_activity(conn, m, comm.id, false, not_author) Mention::from_activity(conn, m, comm.id, false, not_author)
}).ok(); })
.ok();
} }
} }
@ -172,11 +256,14 @@ impl FromActivity<Note, Connection> for Comment {
impl Notify<Connection> for Comment { impl Notify<Connection> for Comment {
fn notify(&self, conn: &Connection) { fn notify(&self, conn: &Connection) {
for author in self.get_post(conn).get_authors(conn) { for author in self.get_post(conn).get_authors(conn) {
Notification::insert(conn, NewNotification { Notification::insert(
kind: notification_kind::COMMENT.to_string(), conn,
object_id: self.id, NewNotification {
user_id: author.id kind: notification_kind::COMMENT.to_string(),
}); object_id: self.id,
user_id: author.id,
},
);
} }
} }
} }

View file

@ -1,7 +1,9 @@
use diesel::{ use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
r2d2::{ConnectionManager, Pool, PooledConnection} use rocket::{
http::Status,
request::{self, FromRequest},
Outcome, Request, State,
}; };
use rocket::{Request, State, Outcome, http::Status, request::{self, FromRequest}};
use std::ops::Deref; use std::ops::Deref;
use Connection; use Connection;
@ -23,7 +25,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
let pool = request.guard::<State<DbPool>>()?; let pool = request.guard::<State<DbPool>>()?;
match pool.get() { match pool.get() {
Ok(conn) => Outcome::Success(DbConn(conn)), Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())) Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
} }
} }
} }

View file

@ -1,12 +1,21 @@
use activitypub::{Actor, activity::{Accept, Follow as FollowAct, Undo}, actor::Person}; use activitypub::{
activity::{Accept, Follow as FollowAct, Undo},
actor::Person,
Actor,
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
use {BASE_URL, ap_url, Connection};
use blogs::Blog; use blogs::Blog;
use notifications::*; use notifications::*;
use users::User; use plume_common::activity_pub::{
broadcast,
inbox::{Deletable, FromActivity, Notify, WithInbox},
sign::Signer,
Id, IntoId,
};
use schema::follows; use schema::follows;
use users::User;
use {ap_url, Connection, BASE_URL};
#[derive(Clone, Queryable, Identifiable, Associations)] #[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(User, foreign_key = "following_id")] #[belongs_to(User, foreign_key = "following_id")]
@ -31,22 +40,35 @@ impl Follow {
find_by!(follows, find_by_ap_url, ap_url as String); find_by!(follows, find_by_ap_url, ap_url as String);
pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> { pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> {
follows::table.filter(follows::follower_id.eq(from)) follows::table
.filter(follows::follower_id.eq(from))
.filter(follows::following_id.eq(to)) .filter(follows::following_id.eq(to))
.get_result(conn) .get_result(conn)
.ok() .ok()
} }
pub fn into_activity(&self, conn: &Connection) -> FollowAct { pub fn into_activity(&self, conn: &Connection) -> FollowAct {
let user = User::get(conn, self.follower_id).expect("Follow::into_activity: actor not found error"); let user = User::get(conn, self.follower_id)
let target = User::get(conn, self.following_id).expect("Follow::into_activity: target not found error"); .expect("Follow::into_activity: actor not found error");
let target = User::get(conn, self.following_id)
.expect("Follow::into_activity: target not found error");
let mut act = FollowAct::default(); let mut act = FollowAct::default();
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).expect("Follow::into_activity: actor error"); act.follow_props
act.follow_props.set_object_object(user.into_activity(&*conn)).expect("Follow::into_activity: object error"); .set_actor_link::<Id>(user.clone().into_id())
act.object_props.set_id_string(self.ap_url.clone()).expect("Follow::into_activity: id error"); .expect("Follow::into_activity: actor error");
act.object_props.set_to_link(target.clone().into_id()).expect("Follow::into_activity: target error"); act.follow_props
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Follow::into_activity: cc error"); .set_object_object(user.into_activity(&*conn))
.expect("Follow::into_activity: object error");
act.object_props
.set_id_string(self.ap_url.clone())
.expect("Follow::into_activity: id error");
act.object_props
.set_to_link(target.clone().into_id())
.expect("Follow::into_activity: target error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Follow::into_activity: cc error");
act act
} }
@ -58,23 +80,41 @@ impl Follow {
target: &A, target: &A,
follow: FollowAct, follow: FollowAct,
from_id: i32, from_id: i32,
target_id: i32 target_id: i32,
) -> Follow { ) -> Follow {
let from_url: String = from.clone().into_id().into(); let from_url: String = from.clone().into_id().into();
let target_url: String = target.clone().into_id().into(); let target_url: String = target.clone().into_id().into();
let res = Follow::insert(conn, NewFollow { let res = Follow::insert(
follower_id: from_id, conn,
following_id: target_id, NewFollow {
ap_url: format!("{}/follow/{}", from_url, target_url), follower_id: from_id,
}); following_id: target_id,
ap_url: format!("{}/follow/{}", from_url, target_url),
},
);
let mut accept = Accept::default(); let mut accept = Accept::default();
let accept_id = ap_url(format!("{}/follow/{}/accept", BASE_URL.as_str(), res.id)); let accept_id = ap_url(format!("{}/follow/{}/accept", BASE_URL.as_str(), res.id));
accept.object_props.set_id_string(accept_id).expect("Follow::accept_follow: id error"); accept
accept.object_props.set_to_link(from.clone().into_id()).expect("Follow::accept_follow: to error"); .object_props
accept.object_props.set_cc_link_vec::<Id>(vec![]).expect("Follow::accept_follow: cc error"); .set_id_string(accept_id)
accept.accept_props.set_actor_link::<Id>(target.clone().into_id()).expect("Follow::accept_follow: actor error"); .expect("Follow::accept_follow: id error");
accept.accept_props.set_object_object(follow).expect("Follow::accept_follow: object error"); accept
.object_props
.set_to_link(from.clone().into_id())
.expect("Follow::accept_follow: to error");
accept
.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Follow::accept_follow: cc error");
accept
.accept_props
.set_actor_link::<Id>(target.clone().into_id())
.expect("Follow::accept_follow: actor error");
accept
.accept_props
.set_object_object(follow)
.expect("Follow::accept_follow: object error");
broadcast(&*target, accept, vec![from.clone()]); broadcast(&*target, accept, vec![from.clone()]);
res res
} }
@ -82,14 +122,41 @@ impl Follow {
impl FromActivity<FollowAct, Connection> for Follow { impl FromActivity<FollowAct, Connection> for Follow {
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow { fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow {
let from_id = follow.follow_props.actor_link::<Id>().map(|l| l.into()) let from_id = follow
.unwrap_or_else(|_| follow.follow_props.actor_object::<Person>().expect("Follow::from_activity: actor not found error").object_props.id_string().expect("Follow::from_activity: actor not found error")); .follow_props
let from = User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error"); .actor_link::<Id>()
match User::from_url(conn, follow.follow_props.object.as_str().expect("Follow::from_activity: target url parsing error").to_string()) { .map(|l| l.into())
.unwrap_or_else(|_| {
follow
.follow_props
.actor_object::<Person>()
.expect("Follow::from_activity: actor not found error")
.object_props
.id_string()
.expect("Follow::from_activity: actor not found error")
});
let from =
User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error");
match User::from_url(
conn,
follow
.follow_props
.object
.as_str()
.expect("Follow::from_activity: target url parsing error")
.to_string(),
) {
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
None => { None => {
let blog = Blog::from_url(conn, follow.follow_props.object.as_str().expect("Follow::from_activity: target url parsing error").to_string()) let blog = Blog::from_url(
.expect("Follow::from_activity: target not found error"); conn,
follow
.follow_props
.object
.as_str()
.expect("Follow::from_activity: target url parsing error")
.to_string(),
).expect("Follow::from_activity: target not found error");
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
} }
} }
@ -98,27 +165,44 @@ impl FromActivity<FollowAct, Connection> for Follow {
impl Notify<Connection> for Follow { impl Notify<Connection> for Follow {
fn notify(&self, conn: &Connection) { fn notify(&self, conn: &Connection) {
Notification::insert(conn, NewNotification { Notification::insert(
kind: notification_kind::FOLLOW.to_string(), conn,
object_id: self.id, NewNotification {
user_id: self.following_id kind: notification_kind::FOLLOW.to_string(),
}); object_id: self.id,
user_id: self.following_id,
},
);
} }
} }
impl Deletable<Connection, Undo> for Follow { impl Deletable<Connection, Undo> for Follow {
fn delete(&self, conn: &Connection) -> Undo { fn delete(&self, conn: &Connection) -> Undo {
diesel::delete(self).execute(conn).expect("Follow::delete: follow deletion error"); diesel::delete(self)
.execute(conn)
.expect("Follow::delete: follow deletion error");
// delete associated notification if any // delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(conn).expect("Follow::delete: notification deletion error"); diesel::delete(&notif)
.execute(conn)
.expect("Follow::delete: notification deletion error");
} }
let mut undo = Undo::default(); let mut undo = Undo::default();
undo.undo_props.set_actor_link(User::get(conn, self.follower_id).expect("Follow::delete: actor error").into_id()).expect("Follow::delete: actor error"); undo.undo_props
undo.object_props.set_id_string(format!("{}/undo", self.ap_url)).expect("Follow::delete: id error"); .set_actor_link(
undo.undo_props.set_object_object(self.into_activity(conn)).expect("Follow::delete: object error"); User::get(conn, self.follower_id)
.expect("Follow::delete: actor error")
.into_id(),
)
.expect("Follow::delete: actor error");
undo.object_props
.set_id_string(format!("{}/undo", self.ap_url))
.expect("Follow::delete: id error");
undo.undo_props
.set_object_object(self.into_activity(conn))
.expect("Follow::delete: object error");
undo undo
} }

View file

@ -0,0 +1,235 @@
use activitypub::{
activity::{Accept, Follow as FollowAct, Undo},
actor::Person,
Actor,
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
<<<<<<< HEAD
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
use {BASE_URL, ap_url, Connection};
=======
>>>>>>> Run rustfmt and rename instanceTests to instance_tests
use blogs::Blog;
use notifications::*;
use plume_common::activity_pub::{
broadcast,
inbox::{Deletable, FromActivity, Notify, WithInbox},
sign::Signer,
Id, IntoId,
};
use schema::follows;
use users::User;
use Connection;
#[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(User, foreign_key = "following_id")]
pub struct Follow {
pub id: i32,
pub follower_id: i32,
pub following_id: i32,
pub ap_url: String,
}
#[derive(Insertable)]
#[table_name = "follows"]
pub struct NewFollow {
pub follower_id: i32,
pub following_id: i32,
pub ap_url: String,
}
impl Follow {
insert!(follows, NewFollow);
get!(follows);
find_by!(follows, find_by_ap_url, ap_url as String);
pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> {
follows::table
.filter(follows::follower_id.eq(from))
.filter(follows::following_id.eq(to))
.get_result(conn)
.ok()
}
pub fn into_activity(&self, conn: &Connection) -> FollowAct {
let user = User::get(conn, self.follower_id)
.expect("Follow::into_activity: actor not found error");
let target = User::get(conn, self.following_id)
.expect("Follow::into_activity: target not found error");
let mut act = FollowAct::default();
act.follow_props
.set_actor_link::<Id>(user.clone().into_id())
.expect("Follow::into_activity: actor error");
act.follow_props
.set_object_object(user.into_activity(&*conn))
.expect("Follow::into_activity: object error");
act.object_props
.set_id_string(self.ap_url.clone())
.expect("Follow::into_activity: id error");
act.object_props
.set_to_link(target.clone().into_id())
.expect("Follow::into_activity: target error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Follow::into_activity: cc error");
act
}
/// from -> The one sending the follow request
/// target -> The target of the request, responding with Accept
pub fn accept_follow<A: Signer + IntoId + Clone, B: Clone + WithInbox + Actor + IntoId>(
conn: &Connection,
from: &B,
target: &A,
follow: FollowAct,
from_id: i32,
target_id: i32,
) -> Follow {
let from_url: String = from.clone().into_id().into();
let target_url: String = target.clone().into_id().into();
let res = Follow::insert(
conn,
NewFollow {
follower_id: from_id,
following_id: target_id,
ap_url: format!("{}/follow/{}", from_url, target_url),
},
);
let mut accept = Accept::default();
<<<<<<< HEAD
let accept_id = ap_url(format!("{}/follow/{}/accept", BASE_URL.as_str(), res.id));
accept.object_props.set_id_string(accept_id).expect("Follow::accept_follow: id error");
accept.object_props.set_to_link(from.clone().into_id()).expect("Follow::accept_follow: to error");
accept.object_props.set_cc_link_vec::<Id>(vec![]).expect("Follow::accept_follow: cc error");
accept.accept_props.set_actor_link::<Id>(target.clone().into_id()).expect("Follow::accept_follow: actor error");
accept.accept_props.set_object_object(follow).expect("Follow::accept_follow: object error");
=======
let accept_id = format!(
"{}#accept",
follow.object_props.id_string().unwrap_or(String::new())
);
accept
.object_props
.set_id_string(accept_id)
.expect("Follow::accept_follow: id error");
accept
.object_props
.set_to_link(from.clone().into_id())
.expect("Follow::accept_follow: to error");
accept
.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Follow::accept_follow: cc error");
accept
.accept_props
.set_actor_link::<Id>(target.clone().into_id())
.expect("Follow::accept_follow: actor error");
accept
.accept_props
.set_object_object(follow)
.expect("Follow::accept_follow: object error");
>>>>>>> Run rustfmt and rename instanceTests to instance_tests
broadcast(&*target, accept, vec![from.clone()]);
res
}
}
impl FromActivity<FollowAct, Connection> for Follow {
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow {
let from_id = follow
.follow_props
.actor_link::<Id>()
.map(|l| l.into())
.unwrap_or_else(|_| {
follow
.follow_props
.actor_object::<Person>()
.expect("Follow::from_activity: actor not found error")
.object_props
.id_string()
.expect("Follow::from_activity: actor not found error")
});
let from =
User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error");
match User::from_url(
conn,
follow
.follow_props
.object
.as_str()
.expect("Follow::from_activity: target url parsing error")
.to_string(),
) {
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
None => {
let blog = Blog::from_url(
conn,
follow
.follow_props
.object
.as_str()
.expect("Follow::from_activity: target url parsing error")
.to_string(),
).expect("Follow::from_activity: target not found error");
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
}
}
}
}
impl Notify<Connection> for Follow {
fn notify(&self, conn: &Connection) {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::FOLLOW.to_string(),
object_id: self.id,
user_id: self.following_id,
},
);
}
}
impl Deletable<Connection, Undo> for Follow {
fn delete(&self, conn: &Connection) -> Undo {
diesel::delete(self)
.execute(conn)
.expect("Follow::delete: follow deletion error");
// delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif)
.execute(conn)
.expect("Follow::delete: notification deletion error");
}
let mut undo = Undo::default();
undo.undo_props
.set_actor_link(
User::get(conn, self.follower_id)
.expect("Follow::delete: actor error")
.into_id(),
)
.expect("Follow::delete: actor error");
undo.object_props
.set_id_string(format!("{}/undo", self.ap_url))
.expect("Follow::delete: id error");
undo.undo_props
.set_object_object(self.into_activity(conn))
.expect("Follow::delete: object error");
undo
}
fn delete_id(id: String, actor_id: String, conn: &Connection) {
if let Some(follow) = Follow::find_by_ap_url(conn, id) {
if let Some(user) = User::find_by_ap_url(conn, actor_id) {
if user.id == follow.follower_id {
follow.delete(conn);
}
}
}
}
}

View file

@ -1,6 +1,8 @@
use rocket::request::{self, FromRequest, Request}; use rocket::request::{self, FromRequest, Request};
use rocket::{http::{Header, HeaderMap}, Outcome}; use rocket::{
http::{Header, HeaderMap},
Outcome,
};
pub struct Headers<'r>(pub HeaderMap<'r>); pub struct Headers<'r>(pub HeaderMap<'r>);
@ -18,10 +20,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> {
} else { } else {
ori.path().to_owned() ori.path().to_owned()
}; };
headers.add(Header::new("(request-target)", headers.add(Header::new(
format!("{} {}", "(request-target)",
request.method().as_str().to_lowercase(), format!("{} {}", request.method().as_str().to_lowercase(), uri),
uri))); ));
Outcome::Success(Headers(headers)) Outcome::Success(Headers(headers))
} }
} }

View file

@ -1,13 +1,13 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::iter::Iterator; use std::iter::Iterator;
use plume_common::utils::md_to_html;
use Connection;
use safe_string::SafeString;
use ap_url; use ap_url;
use users::User; use plume_common::utils::md_to_html;
use safe_string::SafeString;
use schema::{instances, users}; use schema::{instances, users};
use users::User;
use Connection;
#[derive(Clone, Identifiable, Queryable, Serialize)] #[derive(Clone, Identifiable, Queryable, Serialize)]
pub struct Instance { pub struct Instance {
@ -20,12 +20,12 @@ pub struct Instance {
pub open_registrations: bool, pub open_registrations: bool,
pub short_description: SafeString, pub short_description: SafeString,
pub long_description: SafeString, pub long_description: SafeString,
pub default_license : String, pub default_license: String,
pub long_description_html: String, pub long_description_html: String,
pub short_description_html: String pub short_description_html: String,
} }
#[derive(Insertable)] #[derive(Clone, Insertable)]
#[table_name = "instances"] #[table_name = "instances"]
pub struct NewInstance { pub struct NewInstance {
pub public_domain: String, pub public_domain: String,
@ -34,28 +34,32 @@ pub struct NewInstance {
pub open_registrations: bool, pub open_registrations: bool,
pub short_description: SafeString, pub short_description: SafeString,
pub long_description: SafeString, pub long_description: SafeString,
pub default_license : String, pub default_license: String,
pub long_description_html: String, pub long_description_html: String,
pub short_description_html: String pub short_description_html: String,
} }
impl Instance { impl Instance {
pub fn get_local(conn: &Connection) -> Option<Instance> { pub fn get_local(conn: &Connection) -> Option<Instance> {
instances::table.filter(instances::local.eq(true)) instances::table
.filter(instances::local.eq(true))
.limit(1) .limit(1)
.load::<Instance>(conn) .load::<Instance>(conn)
.expect("Instance::get_local: loading error") .expect("Instance::get_local: loading error")
.into_iter().nth(0) .into_iter()
.nth(0)
} }
pub fn get_remotes(conn: &Connection) -> Vec<Instance> { pub fn get_remotes(conn: &Connection) -> Vec<Instance> {
instances::table.filter(instances::local.eq(false)) instances::table
.filter(instances::local.eq(false))
.load::<Instance>(conn) .load::<Instance>(conn)
.expect("Instance::get_remotes: loading error") .expect("Instance::get_remotes: loading error")
} }
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Instance> { pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Instance> {
instances::table.order(instances::public_domain.asc()) instances::table
.order(instances::public_domain.asc())
.offset(min.into()) .offset(min.into())
.limit((max - min).into()) .limit((max - min).into())
.load::<Instance>(conn) .load::<Instance>(conn)
@ -63,7 +67,9 @@ impl Instance {
} }
pub fn local_id(conn: &Connection) -> i32 { pub fn local_id(conn: &Connection) -> i32 {
Instance::get_local(conn).expect("Instance::local_id: local instance not found error").id Instance::get_local(conn)
.expect("Instance::local_id: local instance not found error")
.id
} }
insert!(instances, NewInstance); insert!(instances, NewInstance);
@ -79,10 +85,12 @@ impl Instance {
/// id: AP object id /// id: AP object id
pub fn is_blocked(conn: &Connection, id: String) -> bool { pub fn is_blocked(conn: &Connection, id: String) -> bool {
for block in instances::table.filter(instances::blocked.eq(true)) for block in instances::table
.filter(instances::blocked.eq(true))
.get_results::<Instance>(conn) .get_results::<Instance>(conn)
.expect("Instance::is_blocked: loading error") { .expect("Instance::is_blocked: loading error")
if id.starts_with(format!("https://{}", block.public_domain).as_str()) { {
if id.starts_with(format!("https://{}/", block.public_domain).as_str()) {
return true; return true;
} }
} }
@ -91,7 +99,8 @@ impl Instance {
} }
pub fn has_admin(&self, conn: &Connection) -> bool { pub fn has_admin(&self, conn: &Connection) -> bool {
users::table.filter(users::instance_id.eq(self.id)) users::table
.filter(users::instance_id.eq(self.id))
.filter(users::is_admin.eq(true)) .filter(users::is_admin.eq(true))
.load::<User>(conn) .load::<User>(conn)
.expect("Instance::has_admin: loading error") .expect("Instance::has_admin: loading error")
@ -99,14 +108,20 @@ impl Instance {
} }
pub fn main_admin(&self, conn: &Connection) -> User { pub fn main_admin(&self, conn: &Connection) -> User {
users::table.filter(users::instance_id.eq(self.id)) users::table
.filter(users::instance_id.eq(self.id))
.filter(users::is_admin.eq(true)) .filter(users::is_admin.eq(true))
.limit(1) .limit(1)
.get_result::<User>(conn) .get_result::<User>(conn)
.expect("Instance::main_admin: loading error") .expect("Instance::main_admin: loading error")
} }
pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String { pub fn compute_box(
&self,
prefix: &'static str,
name: String,
box_name: &'static str,
) -> String {
ap_url(format!( ap_url(format!(
"{instance}/{prefix}/{name}/{box_name}", "{instance}/{prefix}/{name}/{box_name}",
instance = self.public_domain, instance = self.public_domain,
@ -116,7 +131,14 @@ impl Instance {
)) ))
} }
pub fn update(&self, conn: &Connection, name: String, open_registrations: bool, short_description: SafeString, long_description: SafeString) { pub fn update(
&self,
conn: &Connection,
name: String,
open_registrations: bool,
short_description: SafeString,
long_description: SafeString,
) {
let (sd, _, _) = md_to_html(short_description.as_ref()); let (sd, _, _) = md_to_html(short_description.as_ref());
let (ld, _, _) = md_to_html(long_description.as_ref()); let (ld, _, _) = md_to_html(long_description.as_ref());
diesel::update(self) diesel::update(self)
@ -126,12 +148,258 @@ impl Instance {
instances::short_description.eq(short_description), instances::short_description.eq(short_description),
instances::long_description.eq(long_description), instances::long_description.eq(long_description),
instances::short_description_html.eq(sd), instances::short_description_html.eq(sd),
instances::long_description_html.eq(ld) instances::long_description_html.eq(ld),
)).execute(conn) ))
.execute(conn)
.expect("Instance::update: update error"); .expect("Instance::update: update error");
} }
pub fn count(conn: &Connection) -> i64 { pub fn count(conn: &Connection) -> i64 {
instances::table.count().get_result(conn).expect("Instance::count: counting error") instances::table
.count()
.get_result(conn)
.expect("Instance::count: counting error")
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use diesel::Connection;
use tests::db;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
vec![
NewInstance {
default_license: "WTFPL".to_string(),
local: true,
long_description: SafeString::new("This is my instance."),
long_description_html: "<p>This is my instance</p>".to_string(),
short_description: SafeString::new("My instance."),
short_description_html: "<p>My instance</p>".to_string(),
name: "My instance".to_string(),
open_registrations: true,
public_domain: "plu.me".to_string(),
},
NewInstance {
default_license: "WTFPL".to_string(),
local: false,
long_description: SafeString::new("This is an instance."),
long_description_html: "<p>This is an instance</p>".to_string(),
short_description: SafeString::new("An instance."),
short_description_html: "<p>An instance</p>".to_string(),
name: "An instance".to_string(),
open_registrations: true,
public_domain: "1plu.me".to_string(),
},
NewInstance {
default_license: "CC-0".to_string(),
local: false,
long_description: SafeString::new("This is the instance of someone."),
long_description_html: "<p>This is the instance of someone</p>".to_string(),
short_description: SafeString::new("Someone instance."),
short_description_html: "<p>Someone instance</p>".to_string(),
name: "Someone instance".to_string(),
open_registrations: false,
public_domain: "2plu.me".to_string(),
},
NewInstance {
default_license: "CC-0-BY-SA".to_string(),
local: false,
long_description: SafeString::new("Good morning"),
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: "Nice day".to_string(),
open_registrations: true,
public_domain: "3plu.me".to_string(),
},
].into_iter()
.map(|inst| {
(
inst.clone(),
Instance::find_by_domain(conn, inst.public_domain.clone())
.unwrap_or_else(|| Instance::insert(conn, inst)),
)
})
.collect()
}
#[test]
fn local_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn)
.into_iter()
.map(|(inserted, _)| inserted)
.find(|inst| inst.local)
.unwrap();
let res = Instance::get_local(conn).unwrap();
part_eq!(
res,
inserted,
[
default_license,
local,
long_description,
long_description_html,
short_description,
short_description_html,
name,
open_registrations,
public_domain
]
);
assert_eq!(Instance::local_id(conn), res.id);
Ok(())
});
}
#[test]
fn remote_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn);
assert_eq!(Instance::count(conn), inserted.len() as i64);
let res = Instance::get_remotes(conn);
assert_eq!(
res.len(),
inserted.iter().filter(|(inst, _)| !inst.local).count()
);
inserted
.iter()
.filter(|(newinst, _)| !newinst.local)
.map(|(newinst, inst)| (newinst, res.iter().find(|res| res.id == inst.id).unwrap()))
.for_each(|(newinst, inst)| {
part_eq!(
newinst,
inst,
[
default_license,
local,
long_description,
long_description_html,
short_description,
short_description_html,
name,
open_registrations,
public_domain
]
)
});
let page = Instance::page(conn, (0, 2));
assert_eq!(page.len(), 2);
let page1 = &page[0];
let page2 = &page[1];
assert!(page1.public_domain <= page2.public_domain);
let mut last_domaine: String = Instance::page(conn, (0, 1))[0].public_domain.clone();
for i in 1..inserted.len() as i32 {
let page = Instance::page(conn, (i, i + 1));
assert_eq!(page.len(), 1);
assert!(last_domaine <= page[0].public_domain);
last_domaine = page[0].public_domain.clone();
}
Ok(())
});
}
#[test]
fn blocked() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inst_list = fill_database(conn);
let inst = &inst_list[0].1;
let inst_list = &inst_list[1..];
let blocked = inst.blocked;
inst.toggle_block(conn);
let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.blocked, !blocked);
assert_eq!(
inst_list
.iter()
.filter(
|(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked
)
.count(),
0
);
assert_eq!(
Instance::is_blocked(conn, format!("https://{}/something", inst.public_domain)),
inst.blocked
);
assert_eq!(
Instance::is_blocked(conn, format!("https://{}a/something", inst.public_domain)),
Instance::find_by_domain(conn, format!("{}a", inst.public_domain))
.map(|inst| inst.blocked)
.unwrap_or(false)
);
inst.toggle_block(conn);
let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.blocked, blocked);
assert_eq!(
Instance::is_blocked(conn, format!("https://{}/something", inst.public_domain)),
inst.blocked
);
assert_eq!(
Instance::is_blocked(conn, format!("https://{}a/something", inst.public_domain)),
Instance::find_by_domain(conn, format!("{}a", inst.public_domain))
.map(|inst| inst.blocked)
.unwrap_or(false)
);
assert_eq!(
inst_list
.iter()
.filter(
|(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked
)
.count(),
0
);
Ok(())
});
}
#[test]
fn update() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inst = &fill_database(conn)[0].1;
inst.update(
conn,
"NewName".to_owned(),
false,
SafeString::new("[short](#link)"),
SafeString::new("[long_description](/with_link)"),
);
let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.name, "NewName".to_owned());
assert_eq!(inst.open_registrations, false);
assert_eq!(
inst.long_description.get(),
"[long_description](/with_link)"
);
assert_eq!(
inst.long_description_html,
"<p><a href=\"/with_link\">long_description</a></p>\n"
);
assert_eq!(inst.short_description.get(), "[short](#link)");
assert_eq!(
inst.short_description_html,
"<p><a href=\"#link\">short</a></p>\n"
);
Ok(())
});
} }
} }

View file

@ -25,6 +25,10 @@ extern crate serde_json;
extern crate url; extern crate url;
extern crate webfinger; extern crate webfinger;
#[cfg(test)]
#[macro_use]
extern crate diesel_migrations;
use std::env; use std::env;
#[cfg(all(feature = "sqlite", not(feature = "postgres")))] #[cfg(all(feature = "sqlite", not(feature = "postgres")))]
@ -99,11 +103,13 @@ macro_rules! list_by {
macro_rules! get { macro_rules! get {
($table:ident) => { ($table:ident) => {
pub fn get(conn: &crate::Connection, id: i32) -> Option<Self> { pub fn get(conn: &crate::Connection, id: i32) -> Option<Self> {
$table::table.filter($table::id.eq(id)) $table::table
.filter($table::id.eq(id))
.limit(1) .limit(1)
.load::<Self>(conn) .load::<Self>(conn)
.expect("macro::get: Error loading $table by id") .expect("macro::get: Error loading $table by id")
.into_iter().nth(0) .into_iter()
.nth(0)
} }
}; };
} }
@ -177,11 +183,16 @@ macro_rules! update {
macro_rules! last { macro_rules! last {
($table:ident) => { ($table:ident) => {
pub fn last(conn: &crate::Connection) -> Self { pub fn last(conn: &crate::Connection) -> Self {
$table::table.order_by($table::id.desc()) $table::table
.order_by($table::id.desc())
.limit(1) .limit(1)
.load::<Self>(conn) .load::<Self>(conn)
.expect(concat!("macro::last: Error getting last ", stringify!($table))) .expect(concat!(
.iter().next() "macro::last: Error getting last ",
stringify!($table)
))
.iter()
.next()
.expect(concat!("macro::last: No last ", stringify!($table))) .expect(concat!("macro::last: No last ", stringify!($table)))
.clone() .clone()
} }
@ -189,31 +200,67 @@ macro_rules! last {
} }
lazy_static! { lazy_static! {
pub static ref BASE_URL: String = env::var("BASE_URL") pub static ref BASE_URL: String = env::var("BASE_URL").unwrap_or(format!(
.unwrap_or(format!("127.0.0.1:{}", env::var("ROCKET_PORT").unwrap_or(String::from("8000")))); "127.0.0.1:{}",
env::var("ROCKET_PORT").unwrap_or(String::from("8000"))
));
pub static ref USE_HTTPS: bool = env::var("USE_HTTPS").map(|val| val == "1").unwrap_or(true); pub static ref USE_HTTPS: bool = env::var("USE_HTTPS").map(|val| val == "1").unwrap_or(true);
} }
#[cfg(not(test))]
static DB_NAME: &str = "plume";
#[cfg(test)]
static DB_NAME: &str = "plume_tests";
#[cfg(all(feature = "postgres", not(feature = "sqlite")))] #[cfg(all(feature = "postgres", not(feature = "sqlite")))]
lazy_static! { lazy_static! {
pub static ref DATABASE_URL: String = env::var("DATABASE_URL").unwrap_or(String::from("postgres://plume:plume@localhost/plume")); pub static ref DATABASE_URL: String =
env::var("DATABASE_URL").unwrap_or(format!("postgres://plume:plume@localhost/{}", DB_NAME));
} }
#[cfg(all(feature = "sqlite", not(feature = "postgres")))] #[cfg(all(feature = "sqlite", not(feature = "postgres")))]
lazy_static! { lazy_static! {
pub static ref DATABASE_URL: String = env::var("DATABASE_URL").unwrap_or(String::from("plume.sqlite")); pub static ref DATABASE_URL: String =
env::var("DATABASE_URL").unwrap_or(format!("{}.sqlite", DB_NAME));
} }
pub fn ap_url(url: String) -> String { pub fn ap_url(url: String) -> String {
let scheme = if *USE_HTTPS { let scheme = if *USE_HTTPS { "https" } else { "http" };
"https"
} else {
"http"
};
format!("{}://{}", scheme, url) format!("{}://{}", scheme, url)
} }
#[cfg(test)]
#[macro_use]
mod tests {
use diesel::Connection;
use Connection as Conn;
use DATABASE_URL;
#[cfg(feature = "sqlite")]
embed_migrations!("../migrations/sqlite");
#[cfg(feature = "postgres")]
embed_migrations!("../migrations/postgres");
#[macro_export]
macro_rules! part_eq {
( $x:expr, $y:expr, [$( $var:ident ),*] ) => {
{
$(
assert_eq!($x.$var, $y.$var);
)*
}
};
}
pub fn db() -> Conn {
let conn =
Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database");
embedded_migrations::run(&conn).expect("Couldn't run migrations");
conn
}
}
pub mod admin; pub mod admin;
pub mod api_tokens; pub mod api_tokens;
pub mod apps; pub mod apps;

View file

@ -1,18 +1,16 @@
use activitypub::activity; use activitypub::activity;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
PUBLIC_VISIBILTY,
Id,
IntoId,
inbox::{FromActivity, Deletable, Notify}
};
use Connection;
use notifications::*; use notifications::*;
use plume_common::activity_pub::{
inbox::{Deletable, FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY,
};
use posts::Post; use posts::Post;
use users::User;
use schema::likes; use schema::likes;
use users::User;
use Connection;
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct Like { pub struct Like {
@ -20,7 +18,7 @@ pub struct Like {
pub user_id: i32, pub user_id: i32,
pub post_id: i32, pub post_id: i32,
pub creation_date: NaiveDateTime, pub creation_date: NaiveDateTime,
pub ap_url: String pub ap_url: String,
} }
#[derive(Default, Insertable)] #[derive(Default, Insertable)]
@ -28,7 +26,7 @@ pub struct Like {
pub struct NewLike { pub struct NewLike {
pub user_id: i32, pub user_id: i32,
pub post_id: i32, pub post_id: i32,
pub ap_url: String pub ap_url: String,
} }
impl Like { impl Like {
@ -45,17 +43,36 @@ impl Like {
User::get(conn, self.user_id).expect("Like::update_ap_url: user error").ap_url, User::get(conn, self.user_id).expect("Like::update_ap_url: user error").ap_url,
Post::get(conn, self.post_id).expect("Like::update_ap_url: post error").ap_url Post::get(conn, self.post_id).expect("Like::update_ap_url: post error").ap_url
))) )))
.execute(conn).expect("Like::update_ap_url: update error"); .execute(conn)
.expect("Like::update_ap_url: update error");
} }
} }
pub fn into_activity(&self, conn: &Connection) -> activity::Like { pub fn into_activity(&self, conn: &Connection) -> activity::Like {
let mut act = activity::Like::default(); let mut act = activity::Like::default();
act.like_props.set_actor_link(User::get(conn, self.user_id).expect("Like::into_activity: user error").into_id()).expect("Like::into_activity: actor error"); act.like_props
act.like_props.set_object_link(Post::get(conn, self.post_id).expect("Like::into_activity: post error").into_id()).expect("Like::into_activity: object error"); .set_actor_link(
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::into_activity: to error"); User::get(conn, self.user_id)
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Like::into_activity: cc error"); .expect("Like::into_activity: user error")
act.object_props.set_id_string(self.ap_url.clone()).expect("Like::into_activity: id error"); .into_id(),
)
.expect("Like::into_activity: actor error");
act.like_props
.set_object_link(
Post::get(conn, self.post_id)
.expect("Like::into_activity: post error")
.into_id(),
)
.expect("Like::into_activity: object error");
act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
.expect("Like::into_activity: to error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Like::into_activity: cc error");
act.object_props
.set_id_string(self.ap_url.clone())
.expect("Like::into_activity: id error");
act act
} }
@ -63,13 +80,30 @@ impl Like {
impl FromActivity<activity::Like, Connection> for Like { impl FromActivity<activity::Like, Connection> for Like {
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like { fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like {
let liker = User::from_url(conn, like.like_props.actor.as_str().expect("Like::from_activity: actor error").to_string()); let liker = User::from_url(
let post = Post::find_by_ap_url(conn, like.like_props.object.as_str().expect("Like::from_activity: object error").to_string()); conn,
let res = Like::insert(conn, NewLike { like.like_props
post_id: post.expect("Like::from_activity: post error").id, .actor
user_id: liker.expect("Like::from_activity: user error").id, .as_str()
ap_url: like.object_props.id_string().unwrap_or(String::from("")) .expect("Like::from_activity: actor error")
}); .to_string(),
);
let post = Post::find_by_ap_url(
conn,
like.like_props
.object
.as_str()
.expect("Like::from_activity: object error")
.to_string(),
);
let res = Like::insert(
conn,
NewLike {
post_id: post.expect("Like::from_activity: post error").id,
user_id: liker.expect("Like::from_activity: user error").id,
ap_url: like.object_props.id_string().unwrap_or(String::from("")),
},
);
res.notify(conn); res.notify(conn);
res res
} }
@ -79,30 +113,51 @@ impl Notify<Connection> for Like {
fn notify(&self, conn: &Connection) { fn notify(&self, conn: &Connection) {
let post = Post::get(conn, self.post_id).expect("Like::notify: post error"); let post = Post::get(conn, self.post_id).expect("Like::notify: post error");
for author in post.get_authors(conn) { for author in post.get_authors(conn) {
Notification::insert(conn, NewNotification { Notification::insert(
kind: notification_kind::LIKE.to_string(), conn,
object_id: self.id, NewNotification {
user_id: author.id kind: notification_kind::LIKE.to_string(),
}); object_id: self.id,
user_id: author.id,
},
);
} }
} }
} }
impl Deletable<Connection, activity::Undo> for Like { impl Deletable<Connection, activity::Undo> for Like {
fn delete(&self, conn: &Connection) -> activity::Undo { fn delete(&self, conn: &Connection) -> activity::Undo {
diesel::delete(self).execute(conn).expect("Like::delete: delete error"); diesel::delete(self)
.execute(conn)
.expect("Like::delete: delete error");
// delete associated notification if any // delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(conn).expect("Like::delete: notification error"); diesel::delete(&notif)
.execute(conn)
.expect("Like::delete: notification error");
} }
let mut act = activity::Undo::default(); let mut act = activity::Undo::default();
act.undo_props.set_actor_link(User::get(conn, self.user_id).expect("Like::delete: user error").into_id()).expect("Like::delete: actor error"); act.undo_props
act.undo_props.set_object_object(self.into_activity(conn)).expect("Like::delete: object error"); .set_actor_link(
act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Like::delete: id error"); User::get(conn, self.user_id)
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::delete: to error"); .expect("Like::delete: user error")
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Like::delete: cc error"); .into_id(),
)
.expect("Like::delete: actor error");
act.undo_props
.set_object_object(self.into_activity(conn))
.expect("Like::delete: object error");
act.object_props
.set_id_string(format!("{}#delete", self.ap_url))
.expect("Like::delete: id error");
act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
.expect("Like::delete: to error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Like::delete: cc error");
act act
} }

View file

@ -1,5 +1,5 @@
use activitypub::object::Image; use activitypub::object::Image;
use diesel::{self, QueryDsl, ExpressionMethods, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID; use guid_create::GUID;
use reqwest; use reqwest;
use serde_json; use serde_json;
@ -7,10 +7,10 @@ use std::{fs, path::Path};
use plume_common::activity_pub::Id; use plume_common::activity_pub::Id;
use {ap_url, Connection};
use instance::Instance; use instance::Instance;
use users::User;
use schema::medias; use schema::medias;
use users::User;
use {ap_url, Connection};
#[derive(Clone, Identifiable, Queryable, Serialize)] #[derive(Clone, Identifiable, Queryable, Serialize)]
pub struct Media { pub struct Media {
@ -21,7 +21,7 @@ pub struct Media {
pub remote_url: Option<String>, pub remote_url: Option<String>,
pub sensitive: bool, pub sensitive: bool,
pub content_warning: Option<String>, pub content_warning: Option<String>,
pub owner_id: i32 pub owner_id: i32,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -33,7 +33,7 @@ pub struct NewMedia {
pub remote_url: Option<String>, pub remote_url: Option<String>,
pub sensitive: bool, pub sensitive: bool,
pub content_warning: Option<String>, pub content_warning: Option<String>,
pub owner_id: i32 pub owner_id: i32,
} }
impl Media { impl Media {
@ -41,29 +41,64 @@ impl Media {
get!(medias); get!(medias);
list_by!(medias, for_user, owner_id as i32); list_by!(medias, for_user, owner_id as i32);
pub fn list_all_medias(conn: &Connection) -> Vec<Media> {
medias::table
.load::<Media>(conn)
.expect("Media::list_all_medias: loading error")
}
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error"); let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error");
let url = self.url(conn); let url = self.url(conn);
let (cat, preview, html, md) = match self.file_path.rsplitn(2, '.').next().expect("Media::to_json: extension error") { let (cat, preview, html, md) = match self
.file_path
.rsplitn(2, '.')
.next()
.expect("Media::to_json: extension error")
{
"png" | "jpg" | "jpeg" | "gif" | "svg" => ( "png" | "jpg" | "jpeg" | "gif" | "svg" => (
"image", "image",
format!("<img src=\"{}\" alt=\"{}\" title=\"{}\" class=\"preview\">", url, self.alt_text, self.alt_text), format!(
format!("<img src=\"{}\" alt=\"{}\" title=\"{}\">", url, self.alt_text, self.alt_text), "<img src=\"{}\" alt=\"{}\" title=\"{}\" class=\"preview\">",
url, self.alt_text, self.alt_text
),
format!(
"<img src=\"{}\" alt=\"{}\" title=\"{}\">",
url, self.alt_text, self.alt_text
),
format!("![{}]({})", self.alt_text, url), format!("![{}]({})", self.alt_text, url),
), ),
"mp3" | "wav" | "flac" => ( "mp3" | "wav" | "flac" => (
"audio", "audio",
format!("<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>", url, self.alt_text), format!(
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text), "<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>",
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text), url, self.alt_text
),
format!(
"<audio src=\"{}\" title=\"{}\"></audio>",
url, self.alt_text
),
format!(
"<audio src=\"{}\" title=\"{}\"></audio>",
url, self.alt_text
),
), ),
"mp4" | "avi" | "webm" | "mov" => ( "mp4" | "avi" | "webm" | "mov" => (
"video", "video",
format!("<video src=\"{}\" title=\"{}\" class=\"preview\"></video>", url, self.alt_text), format!(
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text), "<video src=\"{}\" title=\"{}\" class=\"preview\"></video>",
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text), url, self.alt_text
),
format!(
"<video src=\"{}\" title=\"{}\"></video>",
url, self.alt_text
),
format!(
"<video src=\"{}\" title=\"{}\"></video>",
url, self.alt_text
),
), ),
_ => ("unknown", String::new(), String::new(), String::new()) _ => ("unknown", String::new(), String::new(), String::new()),
}; };
json["html_preview"] = json!(preview); json["html_preview"] = json!(preview);
json["html"] = json!(html); json["html"] = json!(html);
@ -77,30 +112,43 @@ impl Media {
if self.is_remote { if self.is_remote {
self.remote_url.clone().unwrap_or(String::new()) self.remote_url.clone().unwrap_or(String::new())
} else { } else {
ap_url(format!("{}/{}", Instance::get_local(conn).expect("Media::url: local instance not found error").public_domain, self.file_path)) ap_url(format!(
"{}/{}",
Instance::get_local(conn)
.expect("Media::url: local instance not found error")
.public_domain,
self.file_path
))
} }
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) {
fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error"); if !self.is_remote {
diesel::delete(self).execute(conn).expect("Media::delete: database entry deletion error"); fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error");
}
diesel::delete(self)
.execute(conn)
.expect("Media::delete: database entry deletion error");
} }
pub fn save_remote(conn: &Connection, url: String) -> Media { pub fn save_remote(conn: &Connection, url: String, user: &User) -> Media {
Media::insert(conn, NewMedia { Media::insert(
file_path: String::new(), conn,
alt_text: String::new(), NewMedia {
is_remote: true, file_path: String::new(),
remote_url: Some(url), alt_text: String::new(),
sensitive: false, is_remote: true,
content_warning: None, remote_url: Some(url),
owner_id: 1 // It will be owned by the admin during an instant, but set_owner will be called just after sensitive: false,
}) content_warning: None,
owner_id: user.id,
},
)
} }
pub fn set_owner(&self, conn: &Connection, id: i32) { pub fn set_owner(&self, conn: &Connection, user: &User) {
diesel::update(self) diesel::update(self)
.set(medias::owner_id.eq(id)) .set(medias::owner_id.eq(user.id))
.execute(conn) .execute(conn)
.expect("Media::set_owner: owner update error"); .expect("Media::set_owner: owner update error");
} }
@ -108,21 +156,199 @@ impl Media {
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(conn: &Connection, image: Image) -> Option<Media> { pub fn from_activity(conn: &Connection, image: Image) -> Option<Media> {
let remote_url = image.object_props.url_string().ok()?; let remote_url = image.object_props.url_string().ok()?;
let ext = remote_url.rsplit('.').next().map(|ext| ext.to_owned()).unwrap_or("png".to_owned()); let ext = remote_url
let path = Path::new("static").join("media").join(format!("{}.{}", GUID::rand().to_string(), ext)); .rsplit('.')
.next()
.map(|ext| ext.to_owned())
.unwrap_or("png".to_owned());
let path =
Path::new("static")
.join("media")
.join(format!("{}.{}", GUID::rand().to_string(), ext));
let mut dest = fs::File::create(path.clone()).ok()?; let mut dest = fs::File::create(path.clone()).ok()?;
reqwest::get(remote_url.as_str()).ok()? reqwest::get(remote_url.as_str())
.copy_to(&mut dest).ok()?; .ok()?
.copy_to(&mut dest)
.ok()?;
Some(Media::insert(conn, NewMedia { Some(Media::insert(
file_path: path.to_str()?.to_string(), conn,
alt_text: image.object_props.content_string().ok()?, NewMedia {
is_remote: true, file_path: path.to_str()?.to_string(),
remote_url: None, alt_text: image.object_props.content_string().ok()?,
sensitive: image.object_props.summary_string().is_ok(), is_remote: true,
content_warning: image.object_props.summary_string().ok(), remote_url: None,
owner_id: User::from_url(conn, image.object_props.attributed_to_link_vec::<Id>().ok()?.into_iter().next()?.into())?.id sensitive: image.object_props.summary_string().is_ok(),
})) content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_url(
conn,
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.into(),
)?.id,
},
))
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
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<Media> {
let mut wd = current_dir().unwrap().to_path_buf();
while wd.pop() {
if wd.join(".git").exists() {
set_current_dir(wd).unwrap();
break;
}
}
let users = usersTests::fill_database(conn);
let user_one = users[0].id;
let user_two = users[1].id;
let f1 = "static/media/1.png".to_owned();
let f2 = "static/media/2.mp3".to_owned();
fs::write(f1.clone(), []).unwrap();
fs::write(f2.clone(), []).unwrap();
vec![
NewMedia {
file_path: f1,
alt_text: "some alt".to_owned(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: user_one,
},
NewMedia {
file_path: f2,
alt_text: "alt message".to_owned(),
is_remote: false,
remote_url: None,
sensitive: true,
content_warning: Some("Content warning".to_owned()),
owner_id: user_one,
},
NewMedia {
file_path: "".to_owned(),
alt_text: "another alt".to_owned(),
is_remote: true,
remote_url: Some("https://example.com/".to_owned()),
sensitive: false,
content_warning: None,
owner_id: user_two,
},
].into_iter()
.map(|nm| Media::insert(conn, nm))
.collect()
}
pub(crate) fn clean(conn: &Conn) {
//used to remove files generated by tests
for media in Media::list_all_medias(conn) {
media.delete(conn);
}
}
//set_owner
#[test]
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let user = usersTests::fill_database(conn)[0].id;
fill_database(conn);
let path = "static/media/test_deletion".to_owned();
fs::write(path.clone(), []).unwrap();
let media = Media::insert(
conn,
NewMedia {
file_path: path.clone(),
alt_text: "alt message".to_owned(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: user,
},
);
assert!(Path::new(&path).exists());
media.delete(conn);
assert!(!Path::new(&path).exists());
clean(conn);
Ok(())
});
}
#[test]
fn set_owner() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = usersTests::fill_database(conn);
let u1 = &users[0];
let u2 = &users[1];
fill_database(conn);
let path = "static/media/test_set_owner".to_owned();
fs::write(path.clone(), []).unwrap();
let media = Media::insert(
conn,
NewMedia {
file_path: path.clone(),
alt_text: "alt message".to_owned(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: u1.id,
},
);
assert!(
Media::for_user(conn, u1.id)
.iter()
.any(|m| m.id == media.id)
);
assert!(
!Media::for_user(conn, u2.id)
.iter()
.any(|m| m.id == media.id)
);
media.set_owner(conn, u2);
assert!(
!Media::for_user(conn, u1.id)
.iter()
.any(|m| m.id == media.id)
);
assert!(
Media::for_user(conn, u2.id)
.iter()
.any(|m| m.id == media.id)
);
clean(conn);
Ok(())
});
} }
} }

View file

@ -1,13 +1,13 @@
use activitypub::link; use activitypub::link;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::inbox::Notify;
use Connection;
use comments::Comment; use comments::Comment;
use notifications::*; use notifications::*;
use plume_common::activity_pub::inbox::Notify;
use posts::Post; use posts::Post;
use users::User;
use schema::mentions; use schema::mentions;
use users::User;
use Connection;
#[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)] #[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)]
pub struct Mention { pub struct Mention {
@ -15,7 +15,7 @@ pub struct Mention {
pub mentioned_id: i32, pub mentioned_id: i32,
pub post_id: Option<i32>, pub post_id: Option<i32>,
pub comment_id: Option<i32>, pub comment_id: Option<i32>,
pub ap_url: String // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake pub ap_url: String, // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -24,7 +24,7 @@ pub struct NewMention {
pub mentioned_id: i32, pub mentioned_id: i32,
pub post_id: Option<i32>, pub post_id: Option<i32>,
pub comment_id: Option<i32>, pub comment_id: Option<i32>,
pub ap_url: String pub ap_url: String,
} }
impl Mention { impl Mention {
@ -50,38 +50,62 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Option<User> { pub fn get_user(&self, conn: &Connection) -> Option<User> {
match self.get_post(conn) { match self.get_post(conn) {
Some(p) => p.get_authors(conn).into_iter().next(), Some(p) => p.get_authors(conn).into_iter().next(),
None => self.get_comment(conn).map(|c| c.get_author(conn)) None => self.get_comment(conn).map(|c| c.get_author(conn)),
} }
} }
pub fn build_activity(conn: &Connection, ment: String) -> link::Mention { pub fn build_activity(conn: &Connection, ment: String) -> link::Mention {
let user = User::find_by_fqn(conn, ment.clone()); let user = User::find_by_fqn(conn, ment.clone());
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Mention::build_activity: href error"); mention
mention.link_props.set_name_string(format!("@{}", ment)).expect("Mention::build_activity: name error:"); .link_props
.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new()))
.expect("Mention::build_activity: href error");
mention
.link_props
.set_name_string(format!("@{}", ment))
.expect("Mention::build_activity: name error:");
mention mention
} }
pub fn to_activity(&self, conn: &Connection) -> link::Mention { pub fn to_activity(&self, conn: &Connection) -> link::Mention {
let user = self.get_mentioned(conn); let user = self.get_mentioned(conn);
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Mention::to_activity: href error"); mention
mention.link_props.set_name_string(user.map(|u| format!("@{}", u.get_fqn(conn))).unwrap_or(String::new())).expect("Mention::to_activity: mention error"); .link_props
.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new()))
.expect("Mention::to_activity: href error");
mention
.link_props
.set_name_string(
user.map(|u| format!("@{}", u.get_fqn(conn)))
.unwrap_or(String::new()),
)
.expect("Mention::to_activity: mention error");
mention mention
} }
pub fn from_activity(conn: &Connection, ment: link::Mention, inside: i32, in_post: bool, notify: bool) -> Option<Self> { pub fn from_activity(
conn: &Connection,
ment: link::Mention,
inside: i32,
in_post: bool,
notify: bool,
) -> Option<Self> {
let ap_url = ment.link_props.href_string().ok()?; let ap_url = ment.link_props.href_string().ok()?;
let mentioned = User::find_by_ap_url(conn, ap_url)?; let mentioned = User::find_by_ap_url(conn, ap_url)?;
if in_post { if in_post {
Post::get(conn, inside.clone().into()).map(|post| { Post::get(conn, inside.clone().into()).map(|post| {
let res = Mention::insert(conn, NewMention { let res = Mention::insert(
mentioned_id: mentioned.id, conn,
post_id: Some(post.id), NewMention {
comment_id: None, mentioned_id: mentioned.id,
ap_url: ment.link_props.href_string().unwrap_or(String::new()) post_id: Some(post.id),
}); comment_id: None,
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
},
);
if notify { if notify {
res.notify(conn); res.notify(conn);
} }
@ -89,12 +113,15 @@ impl Mention {
}) })
} else { } else {
Comment::get(conn, inside.into()).map(|comment| { Comment::get(conn, inside.into()).map(|comment| {
let res = Mention::insert(conn, NewMention { let res = Mention::insert(
mentioned_id: mentioned.id, conn,
post_id: None, NewMention {
comment_id: Some(comment.id), mentioned_id: mentioned.id,
ap_url: ment.link_props.href_string().unwrap_or(String::new()) post_id: None,
}); comment_id: Some(comment.id),
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
},
);
if notify { if notify {
res.notify(conn); res.notify(conn);
} }
@ -106,18 +133,23 @@ impl Mention {
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) {
//find related notifications and delete them //find related notifications and delete them
Notification::find(conn, notification_kind::MENTION, self.id).map(|n| n.delete(conn)); Notification::find(conn, notification_kind::MENTION, self.id).map(|n| n.delete(conn));
diesel::delete(self).execute(conn).expect("Mention::delete: mention deletion error"); diesel::delete(self)
.execute(conn)
.expect("Mention::delete: mention deletion error");
} }
} }
impl Notify<Connection> for Mention { impl Notify<Connection> for Mention {
fn notify(&self, conn: &Connection) { fn notify(&self, conn: &Connection) {
self.get_mentioned(conn).map(|m| { self.get_mentioned(conn).map(|m| {
Notification::insert(conn, NewNotification { Notification::insert(
kind: notification_kind::MENTION.to_string(), conn,
object_id: self.id, NewNotification {
user_id: m.id kind: notification_kind::MENTION.to_string(),
}); object_id: self.id,
user_id: m.id,
},
);
}); });
} }
} }

View file

@ -1,16 +1,16 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use serde_json; use serde_json;
use Connection;
use comments::Comment; use comments::Comment;
use follows::Follow; use follows::Follow;
use likes::Like; use likes::Like;
use mentions::Mention; use mentions::Mention;
use posts::Post; use posts::Post;
use reshares::Reshare; use reshares::Reshare;
use users::User;
use schema::notifications; use schema::notifications;
use users::User;
use Connection;
pub mod notification_kind { pub mod notification_kind {
pub const COMMENT: &'static str = "COMMENT"; pub const COMMENT: &'static str = "COMMENT";
@ -26,7 +26,7 @@ pub struct Notification {
pub user_id: i32, pub user_id: i32,
pub creation_date: NaiveDateTime, pub creation_date: NaiveDateTime,
pub kind: String, pub kind: String,
pub object_id: i32 pub object_id: i32,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -34,7 +34,7 @@ pub struct Notification {
pub struct NewNotification { pub struct NewNotification {
pub user_id: i32, pub user_id: i32,
pub kind: String, pub kind: String,
pub object_id: i32 pub object_id: i32,
} }
impl Notification { impl Notification {
@ -42,14 +42,20 @@ impl Notification {
get!(notifications); get!(notifications);
pub fn find_for_user(conn: &Connection, user: &User) -> Vec<Notification> { pub fn find_for_user(conn: &Connection, user: &User) -> Vec<Notification> {
notifications::table.filter(notifications::user_id.eq(user.id)) notifications::table
.filter(notifications::user_id.eq(user.id))
.order_by(notifications::creation_date.desc()) .order_by(notifications::creation_date.desc())
.load::<Notification>(conn) .load::<Notification>(conn)
.expect("Notification::find_for_user: notification loading error") .expect("Notification::find_for_user: notification loading error")
} }
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Vec<Notification> { pub fn page_for_user(
notifications::table.filter(notifications::user_id.eq(user.id)) conn: &Connection,
user: &User,
(min, max): (i32, i32),
) -> Vec<Notification> {
notifications::table
.filter(notifications::user_id.eq(user.id))
.order_by(notifications::creation_date.desc()) .order_by(notifications::creation_date.desc())
.offset(min.into()) .offset(min.into())
.limit((max - min).into()) .limit((max - min).into())
@ -58,7 +64,8 @@ impl Notification {
} }
pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Option<Notification> { pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Option<Notification> {
notifications::table.filter(notifications::kind.eq(kind.into())) notifications::table
.filter(notifications::kind.eq(kind.into()))
.filter(notifications::object_id.eq(obj)) .filter(notifications::object_id.eq(obj))
.get_result::<Notification>(conn) .get_result::<Notification>(conn)
.ok() .ok()
@ -67,25 +74,23 @@ impl Notification {
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
let mut json = json!(self); let mut json = json!(self);
json["object"] = json!(match self.kind.as_ref() { json["object"] = json!(match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| json!({
json!({
"post": comment.get_post(conn).to_json(conn), "post": comment.get_post(conn).to_json(conn),
"user": comment.get_author(conn).to_json(conn), "user": comment.get_author(conn).to_json(conn),
"id": comment.id "id": comment.id
}) })),
), notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow| {
notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow|
json!({ json!({
"follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn)) "follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn))
}) })
), }),
notification_kind::LIKE => Like::get(conn, self.object_id).map(|like| notification_kind::LIKE => Like::get(conn, self.object_id).map(|like| {
json!({ json!({
"post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)), "post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)),
"user": User::get(conn, like.user_id).map(|u| u.to_json(conn)) "user": User::get(conn, like.user_id).map(|u| u.to_json(conn))
}) })
), }),
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| {
json!({ json!({
"user": mention.get_user(conn).map(|u| u.to_json(conn)), "user": mention.get_user(conn).map(|u| u.to_json(conn)),
"url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone()) "url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone())
@ -95,19 +100,21 @@ impl Notification {
json!(format!("{}#comment-{}", post["url"].as_str().expect("Notification::to_json: post url error"), comment.id)) json!(format!("{}#comment-{}", post["url"].as_str().expect("Notification::to_json: post url error"), comment.id))
}) })
}) })
), }),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| {
json!({ json!({
"post": reshare.get_post(conn).map(|p| p.to_json(conn)), "post": reshare.get_post(conn).map(|p| p.to_json(conn)),
"user": reshare.get_user(conn).map(|u| u.to_json(conn)) "user": reshare.get_user(conn).map(|u| u.to_json(conn))
}) })
), }),
_ => Some(json!({})) _ => Some(json!({})),
}); });
json json
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) {
diesel::delete(self).execute(conn).expect("Notification::delete: notification deletion error"); diesel::delete(self)
.execute(conn)
.expect("Notification::delete: notification deletion error");
} }
} }

View file

@ -1,8 +1,8 @@
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use posts::Post; use posts::Post;
use users::User;
use schema::post_authors; use schema::post_authors;
use users::User;
#[derive(Clone, Queryable, Identifiable, Associations)] #[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(Post)] #[belongs_to(Post)]
@ -10,14 +10,14 @@ use schema::post_authors;
pub struct PostAuthor { pub struct PostAuthor {
pub id: i32, pub id: i32,
pub post_id: i32, pub post_id: i32,
pub author_id: i32 pub author_id: i32,
} }
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "post_authors"] #[table_name = "post_authors"]
pub struct NewPostAuthor { pub struct NewPostAuthor {
pub post_id: i32, pub post_id: i32,
pub author_id: i32 pub author_id: i32,
} }
impl PostAuthor { impl PostAuthor {

View file

@ -1,36 +1,35 @@
use activitypub::{ use activitypub::{
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
link, link,
object::{Article, Image, Tombstone} object::{Article, Image, Tombstone},
}; };
use canapi::{Error, Provider}; use canapi::{Error, Provider};
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use serde_json; use serde_json;
use plume_api::posts::PostEndpoint;
use plume_common::{
activity_pub::{
Hashtag, Source,
PUBLIC_VISIBILTY, Id, IntoId,
inbox::{Deletable, FromActivity}
},
utils::md_to_html
};
use {BASE_URL, ap_url, Connection};
use blogs::Blog; use blogs::Blog;
use instance::Instance; use instance::Instance;
use likes::Like; use likes::Like;
use medias::Media; use medias::Media;
use mentions::Mention; use mentions::Mention;
use plume_api::posts::PostEndpoint;
use plume_common::{
activity_pub::{
inbox::{Deletable, FromActivity},
Hashtag, Id, IntoId, Source, PUBLIC_VISIBILTY,
},
utils::md_to_html,
};
use post_authors::*; use post_authors::*;
use reshares::Reshare; use reshares::Reshare;
use safe_string::SafeString;
use schema::posts;
use std::collections::HashSet;
use tags::Tag; use tags::Tag;
use users::User; use users::User;
use schema::posts; use {ap_url, Connection, BASE_URL};
use safe_string::SafeString;
use std::collections::HashSet;
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)] #[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")] #[changeset_options(treat_none_as_null = "true")]
@ -68,24 +67,32 @@ pub struct NewPost {
impl<'a> Provider<(&'a Connection, Option<i32>)> for Post { impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
type Data = PostEndpoint; type Data = PostEndpoint;
fn get((conn, user_id): &(&'a Connection, Option<i32>), id: i32) -> Result<PostEndpoint, Error> { fn get(
(conn, user_id): &(&'a Connection, Option<i32>),
id: i32,
) -> Result<PostEndpoint, Error> {
if let Some(post) = Post::get(conn, id) { if let Some(post) = Post::get(conn, id) {
if !post.published && !user_id.map(|u| post.is_author(conn, u)).unwrap_or(false) { if !post.published && !user_id.map(|u| post.is_author(conn, u)).unwrap_or(false) {
return Err(Error::Authorization("You are not authorized to access this post yet.".to_string())) return Err(Error::Authorization(
"You are not authorized to access this post yet.".to_string(),
));
} }
Ok(PostEndpoint { Ok(PostEndpoint {
id: Some(post.id), id: Some(post.id),
title: Some(post.title.clone()), title: Some(post.title.clone()),
subtitle: Some(post.subtitle.clone()), subtitle: Some(post.subtitle.clone()),
content: Some(post.content.get().clone()) content: Some(post.content.get().clone()),
}) })
} else { } else {
Err(Error::NotFound("Request post was not found".to_string())) Err(Error::NotFound("Request post was not found".to_string()))
} }
} }
fn list((conn, user_id): &(&'a Connection, Option<i32>), filter: PostEndpoint) -> Vec<PostEndpoint> { fn list(
(conn, user_id): &(&'a Connection, Option<i32>),
filter: PostEndpoint,
) -> Vec<PostEndpoint> {
let mut query = posts::table.into_boxed(); let mut query = posts::table.into_boxed();
if let Some(title) = filter.title { if let Some(title) = filter.title {
query = query.filter(posts::title.eq(title)); query = query.filter(posts::title.eq(title));
@ -97,23 +104,36 @@ impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
query = query.filter(posts::content.eq(content)); query = query.filter(posts::content.eq(content));
} }
query.get_results::<Post>(*conn).map(|ps| ps.into_iter() query
.filter(|p| p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false)) .get_results::<Post>(*conn)
.map(|p| PostEndpoint { .map(|ps| {
id: Some(p.id), ps.into_iter()
title: Some(p.title.clone()), .filter(|p| {
subtitle: Some(p.subtitle.clone()), p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false)
content: Some(p.content.get().clone()) })
.map(|p| PostEndpoint {
id: Some(p.id),
title: Some(p.title.clone()),
subtitle: Some(p.subtitle.clone()),
content: Some(p.content.get().clone()),
})
.collect()
}) })
.collect() .unwrap_or(vec![])
).unwrap_or(vec![])
} }
fn create((_conn, _user_id): &(&'a Connection, Option<i32>), _query: PostEndpoint) -> Result<PostEndpoint, Error> { fn create(
(_conn, _user_id): &(&'a Connection, Option<i32>),
_query: PostEndpoint,
) -> Result<PostEndpoint, Error> {
unimplemented!() unimplemented!()
} }
fn update((_conn, _user_id): &(&'a Connection, Option<i32>), _id: i32, _new_data: PostEndpoint) -> Result<PostEndpoint, Error> { fn update(
(_conn, _user_id): &(&'a Connection, Option<i32>),
_id: i32,
_new_data: PostEndpoint,
) -> Result<PostEndpoint, Error> {
unimplemented!() unimplemented!()
} }
@ -138,7 +158,8 @@ impl Post {
use schema::tags; use schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table.filter(posts::id.eq_any(ids)) posts::table
.filter(posts::id.eq_any(ids))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.order(posts::creation_date.desc()) .order(posts::creation_date.desc())
.offset(min.into()) .offset(min.into())
@ -150,35 +171,45 @@ impl Post {
pub fn count_for_tag(conn: &Connection, tag: String) -> i64 { pub fn count_for_tag(conn: &Connection, tag: String) -> i64 {
use schema::tags; use schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
*posts::table.filter(posts::id.eq_any(ids)) *posts::table
.filter(posts::id.eq_any(ids))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.count() .count()
.load(conn) .load(conn)
.expect("Post::count_for_tag: counting error") .expect("Post::count_for_tag: counting error")
.iter().next().expect("Post::count_for_tag: no result error") .iter()
.next()
.expect("Post::count_for_tag: no result error")
} }
pub fn count_local(conn: &Connection) -> usize { pub fn count_local(conn: &Connection) -> usize {
use schema::post_authors; use schema::post_authors;
use schema::users; use schema::users;
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); let local_authors = users::table
let local_posts_id = post_authors::table.filter(post_authors::author_id.eq_any(local_authors)).select(post_authors::post_id); .filter(users::instance_id.eq(Instance::local_id(conn)))
posts::table.filter(posts::id.eq_any(local_posts_id)) .select(users::id);
let local_posts_id = post_authors::table
.filter(post_authors::author_id.eq_any(local_authors))
.select(post_authors::post_id);
posts::table
.filter(posts::id.eq_any(local_posts_id))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.load::<Post>(conn) .load::<Post>(conn)
.expect("Post::count_local: loading error") .expect("Post::count_local: loading error")
.len()// TODO count in database? .len() // TODO count in database?
} }
pub fn count(conn: &Connection) -> i64 { pub fn count(conn: &Connection) -> i64 {
posts::table.filter(posts::published.eq(true)) posts::table
.filter(posts::published.eq(true))
.count() .count()
.get_result(conn) .get_result(conn)
.expect("Post::count: counting error") .expect("Post::count: counting error")
} }
pub fn get_recents(conn: &Connection, limit: i64) -> Vec<Post> { pub fn get_recents(conn: &Connection, limit: i64) -> Vec<Post> {
posts::table.order(posts::creation_date.desc()) posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.limit(limit) .limit(limit)
.load::<Post>(conn) .load::<Post>(conn)
@ -189,7 +220,8 @@ impl Post {
use schema::post_authors; use schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table.filter(posts::id.eq_any(posts)) posts::table
.filter(posts::id.eq_any(posts))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.order(posts::creation_date.desc()) .order(posts::creation_date.desc())
.limit(limit) .limit(limit)
@ -198,7 +230,8 @@ impl Post {
} }
pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Vec<Post> { pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Vec<Post> {
posts::table.filter(posts::blog_id.eq(blog.id)) posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.order(posts::creation_date.desc()) .order(posts::creation_date.desc())
.limit(limit) .limit(limit)
@ -206,15 +239,17 @@ impl Post {
.expect("Post::get_recents_for_blog: loading error") .expect("Post::get_recents_for_blog: loading error")
} }
pub fn get_for_blog(conn: &Connection, blog:&Blog) -> Vec<Post> { pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Vec<Post> {
posts::table.filter(posts::blog_id.eq(blog.id)) posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.load::<Post>(conn) .load::<Post>(conn)
.expect("Post::get_for_blog:: loading error") .expect("Post::get_for_blog:: loading error")
} }
pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Vec<Post> { pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Vec<Post> {
posts::table.filter(posts::blog_id.eq(blog.id)) posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.order(posts::creation_date.desc()) .order(posts::creation_date.desc())
.offset(min.into()) .offset(min.into())
@ -225,7 +260,8 @@ impl Post {
/// Give a page of all the recent posts known to this instance (= federated timeline) /// Give a page of all the recent posts known to this instance (= federated timeline)
pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Post> { pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Post> {
posts::table.order(posts::creation_date.desc()) posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.offset(min.into()) .offset(min.into())
.limit((max - min).into()) .limit((max - min).into())
@ -234,12 +270,19 @@ impl Post {
} }
/// Give a page of posts from a specific instance /// Give a page of posts from a specific instance
pub fn get_instance_page(conn: &Connection, instance_id: i32, (min, max): (i32, i32)) -> Vec<Post> { pub fn get_instance_page(
conn: &Connection,
instance_id: i32,
(min, max): (i32, i32),
) -> Vec<Post> {
use schema::blogs; use schema::blogs;
let blog_ids = blogs::table.filter(blogs::instance_id.eq(instance_id)).select(blogs::id); let blog_ids = blogs::table
.filter(blogs::instance_id.eq(instance_id))
.select(blogs::id);
posts::table.order(posts::creation_date.desc()) posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.filter(posts::blog_id.eq_any(blog_ids)) .filter(posts::blog_id.eq_any(blog_ids))
.offset(min.into()) .offset(min.into())
@ -249,13 +292,18 @@ impl Post {
} }
/// Give a page of customized user feed, based on a list of followed users /// Give a page of customized user feed, based on a list of followed users
pub fn user_feed_page(conn: &Connection, followed: Vec<i32>, (min, max): (i32, i32)) -> Vec<Post> { pub fn user_feed_page(
conn: &Connection,
followed: Vec<i32>,
(min, max): (i32, i32),
) -> Vec<Post> {
use schema::post_authors; use schema::post_authors;
let post_ids = post_authors::table let post_ids = post_authors::table
.filter(post_authors::author_id.eq_any(followed)) .filter(post_authors::author_id.eq_any(followed))
.select(post_authors::post_id); .select(post_authors::post_id);
posts::table.order(posts::creation_date.desc()) posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.filter(posts::id.eq_any(post_ids)) .filter(posts::id.eq_any(post_ids))
.offset(min.into()) .offset(min.into())
@ -268,7 +316,8 @@ impl Post {
use schema::post_authors; use schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table.order(posts::creation_date.desc()) posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(false)) .filter(posts::published.eq(false))
.filter(posts::id.eq_any(posts)) .filter(posts::id.eq_any(posts))
.load::<Post>(conn) .load::<Post>(conn)
@ -276,10 +325,13 @@ impl Post {
} }
pub fn get_authors(&self, conn: &Connection) -> Vec<User> { pub fn get_authors(&self, conn: &Connection) -> Vec<User> {
use schema::users;
use schema::post_authors; use schema::post_authors;
use schema::users;
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id); let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
users::table.filter(users::id.eq_any(author_list)).load::<User>(conn).expect("Post::get_authors: loading error") users::table
.filter(users::id.eq_any(author_list))
.load::<User>(conn)
.expect("Post::get_authors: loading error")
} }
pub fn is_author(&self, conn: &Connection, author_id: i32) -> bool { pub fn is_author(&self, conn: &Connection, author_id: i32) -> bool {
@ -293,23 +345,28 @@ impl Post {
pub fn get_blog(&self, conn: &Connection) -> Blog { pub fn get_blog(&self, conn: &Connection) -> Blog {
use schema::blogs; use schema::blogs;
blogs::table.filter(blogs::id.eq(self.blog_id)) blogs::table
.filter(blogs::id.eq(self.blog_id))
.limit(1) .limit(1)
.load::<Blog>(conn) .load::<Blog>(conn)
.expect("Post::get_blog: loading error") .expect("Post::get_blog: loading error")
.into_iter().nth(0).expect("Post::get_blog: no result error") .into_iter()
.nth(0)
.expect("Post::get_blog: no result error")
} }
pub fn get_likes(&self, conn: &Connection) -> Vec<Like> { pub fn get_likes(&self, conn: &Connection) -> Vec<Like> {
use schema::likes; use schema::likes;
likes::table.filter(likes::post_id.eq(self.id)) likes::table
.filter(likes::post_id.eq(self.id))
.load::<Like>(conn) .load::<Like>(conn)
.expect("Post::get_likes: loading error") .expect("Post::get_likes: loading error")
} }
pub fn get_reshares(&self, conn: &Connection) -> Vec<Reshare> { pub fn get_reshares(&self, conn: &Connection) -> Vec<Reshare> {
use schema::reshares; use schema::reshares;
reshares::table.filter(reshares::post_id.eq(self.id)) reshares::table
.filter(reshares::post_id.eq(self.id))
.load::<Reshare>(conn) .load::<Reshare>(conn)
.expect("Post::get_reshares: loading error") .expect("Post::get_reshares: loading error")
} }
@ -318,7 +375,8 @@ impl Post {
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(posts::ap_url.eq(self.compute_id(conn))) .set(posts::ap_url.eq(self.compute_id(conn)))
.execute(conn).expect("Post::update_ap_url: update error"); .execute(conn)
.expect("Post::update_ap_url: update error");
Post::get(conn, self.id).expect("Post::update_ap_url: get error") Post::get(conn, self.id).expect("Post::update_ap_url: get error")
} else { } else {
self.clone() self.clone()
@ -326,7 +384,11 @@ impl Post {
} }
pub fn get_receivers_urls(&self, conn: &Connection) -> Vec<String> { pub fn get_receivers_urls(&self, conn: &Connection) -> Vec<String> {
let followers = self.get_authors(conn).into_iter().map(|a| a.get_followers(conn)).collect::<Vec<Vec<User>>>(); let followers = self
.get_authors(conn)
.into_iter()
.map(|a| a.get_followers(conn))
.collect::<Vec<Vec<User>>>();
let to = followers.into_iter().fold(vec![], |mut acc, f| { let to = followers.into_iter().fold(vec![], |mut acc, f| {
for x in f { for x in f {
acc.push(x.ap_url); acc.push(x.ap_url);
@ -340,74 +402,170 @@ impl Post {
let mut to = self.get_receivers_urls(conn); let mut to = self.get_receivers_urls(conn);
to.push(PUBLIC_VISIBILTY.to_string()); to.push(PUBLIC_VISIBILTY.to_string());
let mut mentions_json = Mention::list_for_post(conn, self.id).into_iter().map(|m| json!(m.to_activity(conn))).collect::<Vec<serde_json::Value>>(); let mut mentions_json = Mention::list_for_post(conn, self.id)
let mut tags_json = Tag::for_post(conn, self.id).into_iter().map(|t| json!(t.into_activity(conn))).collect::<Vec<serde_json::Value>>(); .into_iter()
.map(|m| json!(m.to_activity(conn)))
.collect::<Vec<serde_json::Value>>();
let mut tags_json = Tag::for_post(conn, self.id)
.into_iter()
.map(|t| json!(t.into_activity(conn)))
.collect::<Vec<serde_json::Value>>();
mentions_json.append(&mut tags_json); mentions_json.append(&mut tags_json);
let mut article = Article::default(); let mut article = Article::default();
article.object_props.set_name_string(self.title.clone()).expect("Post::into_activity: name error"); article
article.object_props.set_id_string(self.ap_url.clone()).expect("Post::into_activity: id error"); .object_props
.set_name_string(self.title.clone())
.expect("Post::into_activity: name error");
article
.object_props
.set_id_string(self.ap_url.clone())
.expect("Post::into_activity: id error");
let mut authors = self.get_authors(conn).into_iter().map(|x| Id::new(x.ap_url)).collect::<Vec<Id>>(); let mut authors = self
.get_authors(conn)
.into_iter()
.map(|x| Id::new(x.ap_url))
.collect::<Vec<Id>>();
authors.push(self.get_blog(conn).into_id()); // add the blog URL here too authors.push(self.get_blog(conn).into_id()); // add the blog URL here too
article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Post::into_activity: attributedTo error"); article
article.object_props.set_content_string(self.content.get().clone()).expect("Post::into_activity: content error"); .object_props
article.ap_object_props.set_source_object(Source { .set_attributed_to_link_vec::<Id>(authors)
content: self.source.clone(), .expect("Post::into_activity: attributedTo error");
media_type: String::from("text/markdown"), article
}).expect("Post::into_activity: source error"); .object_props
article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Post::into_activity: published error"); .set_content_string(self.content.get().clone())
article.object_props.set_summary_string(self.subtitle.clone()).expect("Post::into_activity: summary error"); .expect("Post::into_activity: content error");
article
.ap_object_props
.set_source_object(Source {
content: self.source.clone(),
media_type: String::from("text/markdown"),
})
.expect("Post::into_activity: source error");
article
.object_props
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))
.expect("Post::into_activity: published error");
article
.object_props
.set_summary_string(self.subtitle.clone())
.expect("Post::into_activity: summary error");
article.object_props.tag = Some(json!(mentions_json)); article.object_props.tag = Some(json!(mentions_json));
if let Some(media_id) = self.cover_id { if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error"); let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error");
let mut cover = Image::default(); let mut cover = Image::default();
cover.object_props.set_url_string(media.url(conn)).expect("Post::into_activity: icon.url error"); cover
.object_props
.set_url_string(media.url(conn))
.expect("Post::into_activity: icon.url error");
if media.sensitive { if media.sensitive {
cover.object_props.set_summary_string(media.content_warning.unwrap_or(String::new())).expect("Post::into_activity: icon.summary error"); cover
.object_props
.set_summary_string(media.content_warning.unwrap_or(String::new()))
.expect("Post::into_activity: icon.summary error");
} }
cover.object_props.set_content_string(media.alt_text).expect("Post::into_activity: icon.content error"); cover
cover.object_props.set_attributed_to_link_vec(vec![ .object_props
User::get(conn, media.owner_id).expect("Post::into_activity: media owner not found").into_id() .set_content_string(media.alt_text)
]).expect("Post::into_activity: icon.attributedTo error"); .expect("Post::into_activity: icon.content error");
article.object_props.set_icon_object(cover).expect("Post::into_activity: icon error"); cover
.object_props
.set_attributed_to_link_vec(vec![
User::get(conn, media.owner_id)
.expect("Post::into_activity: media owner not found")
.into_id(),
])
.expect("Post::into_activity: icon.attributedTo error");
article
.object_props
.set_icon_object(cover)
.expect("Post::into_activity: icon error");
} }
article.object_props.set_url_string(self.ap_url.clone()).expect("Post::into_activity: url error"); article
article.object_props.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect()).expect("Post::into_activity: to error"); .object_props
article.object_props.set_cc_link_vec::<Id>(vec![]).expect("Post::into_activity: cc error"); .set_url_string(self.ap_url.clone())
.expect("Post::into_activity: url error");
article
.object_props
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())
.expect("Post::into_activity: to error");
article
.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Post::into_activity: cc error");
article article
} }
pub fn create_activity(&self, conn: &Connection) -> Create { pub fn create_activity(&self, conn: &Connection) -> Create {
let article = self.into_activity(conn); let article = self.into_activity(conn);
let mut act = Create::default(); let mut act = Create::default();
act.object_props.set_id_string(format!("{}activity", self.ap_url)).expect("Post::create_activity: id error"); act.object_props
act.object_props.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::create_activity: Couldn't copy 'to'")) .set_id_string(format!("{}activity", self.ap_url))
.expect("Post::create_activity: id error");
act.object_props
.set_to_link_vec::<Id>(
article
.object_props
.to_link_vec()
.expect("Post::create_activity: Couldn't copy 'to'"),
)
.expect("Post::create_activity: to error"); .expect("Post::create_activity: to error");
act.object_props.set_cc_link_vec::<Id>(article.object_props.cc_link_vec().expect("Post::create_activity: Couldn't copy 'cc'")) act.object_props
.set_cc_link_vec::<Id>(
article
.object_props
.cc_link_vec()
.expect("Post::create_activity: Couldn't copy 'cc'"),
)
.expect("Post::create_activity: cc error"); .expect("Post::create_activity: cc error");
act.create_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::create_activity: actor error"); act.create_props
act.create_props.set_object_object(article).expect("Post::create_activity: object error"); .set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url))
.expect("Post::create_activity: actor error");
act.create_props
.set_object_object(article)
.expect("Post::create_activity: object error");
act act
} }
pub fn update_activity(&self, conn: &Connection) -> Update { pub fn update_activity(&self, conn: &Connection) -> Update {
let article = self.into_activity(conn); let article = self.into_activity(conn);
let mut act = Update::default(); let mut act = Update::default();
act.object_props.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp())).expect("Post::update_activity: id error"); act.object_props
act.object_props.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::update_activity: Couldn't copy 'to'")) .set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp()))
.expect("Post::update_activity: id error");
act.object_props
.set_to_link_vec::<Id>(
article
.object_props
.to_link_vec()
.expect("Post::update_activity: Couldn't copy 'to'"),
)
.expect("Post::update_activity: to error"); .expect("Post::update_activity: to error");
act.object_props.set_cc_link_vec::<Id>(article.object_props.cc_link_vec().expect("Post::update_activity: Couldn't copy 'cc'")) act.object_props
.set_cc_link_vec::<Id>(
article
.object_props
.cc_link_vec()
.expect("Post::update_activity: Couldn't copy 'cc'"),
)
.expect("Post::update_activity: cc error"); .expect("Post::update_activity: cc error");
act.update_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::update_activity: actor error"); act.update_props
act.update_props.set_object_object(article).expect("Article::update_activity: object error"); .set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url))
.expect("Post::update_activity: actor error");
act.update_props
.set_object_object(article)
.expect("Article::update_activity: object error");
act act
} }
pub fn handle_update(conn: &Connection, updated: Article) { pub fn handle_update(conn: &Connection, updated: Article) {
let id = updated.object_props.id_string().expect("Post::handle_update: id error"); let id = updated
.object_props
.id_string()
.expect("Post::handle_update: id error");
let mut post = Post::find_by_ap_url(conn, id).expect("Post::handle_update: finding error"); let mut post = Post::find_by_ap_url(conn, id).expect("Post::handle_update: finding error");
if let Ok(title) = updated.object_props.name_string() { if let Ok(title) = updated.object_props.name_string() {
@ -431,7 +589,11 @@ impl Post {
post.source = source.content; post.source = source.content;
} }
let mut txt_hashtags = md_to_html(&post.source).2.into_iter().map(|s| s.to_camel_case()).collect::<HashSet<_>>(); let mut txt_hashtags = md_to_html(&post.source)
.2
.into_iter()
.map(|s| s.to_camel_case())
.collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(mention_tags)) = updated.object_props.tag.clone() { if let Some(serde_json::Value::Array(mention_tags)) = updated.object_props.tag.clone() {
let mut mentions = vec![]; let mut mentions = vec![];
let mut tags = vec![]; let mut tags = vec![];
@ -443,13 +605,16 @@ impl Post {
serde_json::from_value::<Hashtag>(tag.clone()) serde_json::from_value::<Hashtag>(tag.clone())
.map(|t| { .map(|t| {
let tag_name = t.name_string().expect("Post::from_activity: tag name error"); let tag_name = t
.name_string()
.expect("Post::from_activity: tag name error");
if txt_hashtags.remove(&tag_name) { if txt_hashtags.remove(&tag_name) {
hashtags.push(t); hashtags.push(t);
} else { } else {
tags.push(t); tags.push(t);
} }
}).ok(); })
.ok();
} }
post.update_mentions(conn, mentions); post.update_mentions(conn, mentions);
post.update_tags(conn, tags); post.update_tags(conn, tags);
@ -460,34 +625,76 @@ impl Post {
} }
pub fn update_mentions(&self, conn: &Connection, mentions: Vec<link::Mention>) { pub fn update_mentions(&self, conn: &Connection, mentions: Vec<link::Mention>) {
let mentions = mentions.into_iter().map(|m| (m.link_props.href_string().ok() let mentions = mentions
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url)) .into_iter()
.map(|u| u.id),m)) .map(|m| {
.filter_map(|(id, m)| if let Some(id)=id {Some((m,id))} else {None}).collect::<Vec<_>>(); (
m.link_props
.href_string()
.ok()
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url))
.map(|u| u.id),
m,
)
})
.filter_map(|(id, m)| {
if let Some(id) = id {
Some((m, id))
} else {
None
}
})
.collect::<Vec<_>>();
let old_mentions = Mention::list_for_post(&conn, self.id); let old_mentions = Mention::list_for_post(&conn, self.id);
let old_user_mentioned = old_mentions.iter() let old_user_mentioned = old_mentions
.map(|m| m.mentioned_id).collect::<HashSet<_>>(); .iter()
for (m,id) in mentions.iter() { .map(|m| m.mentioned_id)
if !old_user_mentioned.contains(&id) { .collect::<HashSet<_>>();
for (m, id) in mentions.iter() {
if !old_user_mentioned.contains(&id) {
Mention::from_activity(&*conn, m.clone(), self.id, true, true); Mention::from_activity(&*conn, m.clone(), self.id, true, true);
} }
} }
let new_mentions = mentions.into_iter().map(|(_m,id)| id).collect::<HashSet<_>>(); let new_mentions = mentions
for m in old_mentions.iter().filter(|m| !new_mentions.contains(&m.mentioned_id)) { .into_iter()
.map(|(_m, id)| id)
.collect::<HashSet<_>>();
for m in old_mentions
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(&conn); m.delete(&conn);
} }
} }
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) { pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) {
let tags_name = tags.iter().filter_map(|t| t.name_string().ok()).collect::<HashSet<_>>(); let tags_name = tags
.iter()
.filter_map(|t| t.name_string().ok())
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id).into_iter().collect::<Vec<_>>(); let old_tags = Tag::for_post(&*conn, self.id)
let old_tags_name = old_tags.iter().filter_map(|tag| if !tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>(); .into_iter()
.collect::<Vec<_>>();
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
if !tag.is_hashtag {
Some(tag.tag.clone())
} else {
None
}
})
.collect::<HashSet<_>>();
for t in tags.into_iter() { for t in tags.into_iter() {
if !t.name_string().map(|n| old_tags_name.contains(&n)).unwrap_or(true) { if !t
.name_string()
.map(|n| old_tags_name.contains(&n))
.unwrap_or(true)
{
Tag::from_activity(conn, t, self.id, false); Tag::from_activity(conn, t, self.id, false);
} }
} }
@ -500,13 +707,31 @@ impl Post {
} }
pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) { pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) {
let tags_name = tags.iter().filter_map(|t| t.name_string().ok()).collect::<HashSet<_>>(); let tags_name = tags
.iter()
.filter_map(|t| t.name_string().ok())
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id).into_iter().collect::<Vec<_>>(); let old_tags = Tag::for_post(&*conn, self.id)
let old_tags_name = old_tags.iter().filter_map(|tag| if tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>(); .into_iter()
.collect::<Vec<_>>();
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
if tag.is_hashtag {
Some(tag.tag.clone())
} else {
None
}
})
.collect::<HashSet<_>>();
for t in tags.into_iter() { for t in tags.into_iter() {
if !t.name_string().map(|n| old_tags_name.contains(&n)).unwrap_or(true) { if !t
.name_string()
.map(|n| old_tags_name.contains(&n))
.unwrap_or(true)
{
Tag::from_activity(conn, t, self.id, true); Tag::from_activity(conn, t, self.id, true);
} }
} }
@ -532,16 +757,26 @@ impl Post {
} }
pub fn compute_id(&self, conn: &Connection) -> String { pub fn compute_id(&self, conn: &Connection) -> String {
ap_url(format!("{}/~/{}/{}/", BASE_URL.as_str(), self.get_blog(conn).get_fqn(conn), self.slug)) ap_url(format!(
"{}/~/{}/{}/",
BASE_URL.as_str(),
self.get_blog(conn).get_fqn(conn),
self.slug
))
} }
} }
impl FromActivity<Article, Connection> for Post { impl FromActivity<Article, Connection> for Post {
fn from_activity(conn: &Connection, article: Article, _actor: Id) -> Post { fn from_activity(conn: &Connection, article: Article, _actor: Id) -> Post {
if let Some(post) = Post::find_by_ap_url(conn, article.object_props.id_string().unwrap_or(String::new())) { if let Some(post) = Post::find_by_ap_url(
conn,
article.object_props.id_string().unwrap_or(String::new()),
) {
post post
} else { } else {
let (blog, authors) = article.object_props.attributed_to_link_vec::<Id>() let (blog, authors) = article
.object_props
.attributed_to_link_vec::<Id>()
.expect("Post::from_activity: attributedTo error") .expect("Post::from_activity: attributedTo error")
.into_iter() .into_iter()
.fold((None, vec![]), |(blog, mut authors), link| { .fold((None, vec![]), |(blog, mut authors), link| {
@ -550,39 +785,78 @@ impl FromActivity<Article, Connection> for Post {
Some(user) => { Some(user) => {
authors.push(user); authors.push(user);
(blog, authors) (blog, authors)
}, }
None => (blog.or_else(|| Blog::from_url(conn, url)), authors) None => (blog.or_else(|| Blog::from_url(conn, url)), authors),
} }
}); });
let cover = article.object_props.icon_object::<Image>().ok() let cover = article
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(conn, img).map(|m| m.id)); .and_then(|img| Media::from_activity(conn, img).map(|m| m.id));
let title = article.object_props.name_string().expect("Post::from_activity: title error"); let title = article
let post = Post::insert(conn, NewPost { .object_props
blog_id: blog.expect("Post::from_activity: blog not found error").id, .name_string()
slug: title.to_kebab_case(), .expect("Post::from_activity: title error");
title: title, let post = Post::insert(
content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")), conn,
published: true, NewPost {
license: String::from("CC-BY-SA"), // TODO blog_id: blog.expect("Post::from_activity: blog not found error").id,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields slug: title.to_kebab_case(),
ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")), title: title,
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()), content: SafeString::new(
subtitle: article.object_props.summary_string().expect("Post::from_activity: summary error"), &article
source: article.ap_object_props.source_object::<Source>().expect("Post::from_activity: source error").content, .object_props
cover_id: cover, .content_string()
}); .expect("Post::from_activity: content error"),
),
published: true,
license: String::from("CC-BY-SA"), // TODO
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article.object_props.url_string().unwrap_or(
article
.object_props
.id_string()
.expect("Post::from_activity: url + id error"),
),
creation_date: Some(
article
.object_props
.published_utctime()
.expect("Post::from_activity: published error")
.naive_utc(),
),
subtitle: article
.object_props
.summary_string()
.expect("Post::from_activity: summary error"),
source: article
.ap_object_props
.source_object::<Source>()
.expect("Post::from_activity: source error")
.content,
cover_id: cover,
},
);
for author in authors.into_iter() { for author in authors.into_iter() {
PostAuthor::insert(conn, NewPostAuthor { PostAuthor::insert(
post_id: post.id, conn,
author_id: author.id NewPostAuthor {
}); post_id: post.id,
author_id: author.id,
},
);
} }
// save mentions and tags // save mentions and tags
let mut hashtags = md_to_html(&post.source).2.into_iter().map(|s| s.to_camel_case()).collect::<HashSet<_>>(); let mut hashtags = md_to_html(&post.source)
.2
.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.clone() {
for tag in tags.into_iter() { for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag.clone()) serde_json::from_value::<link::Mention>(tag.clone())
@ -591,7 +865,9 @@ impl FromActivity<Article, Connection> for Post {
serde_json::from_value::<Hashtag>(tag.clone()) serde_json::from_value::<Hashtag>(tag.clone())
.map(|t| { .map(|t| {
let tag_name = t.name_string().expect("Post::from_activity: tag name error"); let tag_name = t
.name_string()
.expect("Post::from_activity: tag name error");
Tag::from_activity(conn, t, post.id, hashtags.remove(&tag_name)); Tag::from_activity(conn, t, post.id, hashtags.remove(&tag_name));
}) })
.ok(); .ok();
@ -605,28 +881,44 @@ impl FromActivity<Article, Connection> for Post {
impl Deletable<Connection, Delete> for Post { impl Deletable<Connection, Delete> for Post {
fn delete(&self, conn: &Connection) -> Delete { fn delete(&self, conn: &Connection) -> Delete {
let mut act = Delete::default(); let mut act = Delete::default();
act.delete_props.set_actor_link(self.get_authors(conn)[0].clone().into_id()).expect("Post::delete: actor error"); act.delete_props
.set_actor_link(self.get_authors(conn)[0].clone().into_id())
.expect("Post::delete: actor error");
let mut tombstone = Tombstone::default(); let mut tombstone = Tombstone::default();
tombstone.object_props.set_id_string(self.ap_url.clone()).expect("Post::delete: object.id error"); tombstone
act.delete_props.set_object_object(tombstone).expect("Post::delete: object error"); .object_props
.set_id_string(self.ap_url.clone())
.expect("Post::delete: object.id error");
act.delete_props
.set_object_object(tombstone)
.expect("Post::delete: object error");
act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Post::delete: id error"); act.object_props
act.object_props.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]).expect("Post::delete: to error"); .set_id_string(format!("{}#delete", self.ap_url))
.expect("Post::delete: id error");
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)])
.expect("Post::delete: to error");
for m in Mention::list_for_post(&conn, self.id) { for m in Mention::list_for_post(&conn, self.id) {
m.delete(conn); m.delete(conn);
} }
diesel::delete(self).execute(conn).expect("Post::delete: DB error"); diesel::delete(self)
.execute(conn)
.expect("Post::delete: DB error");
act act
} }
fn delete_id(id: String, actor_id: String, conn: &Connection) { fn delete_id(id: String, actor_id: String, conn: &Connection) {
let actor = User::find_by_ap_url(conn, actor_id); let actor = User::find_by_ap_url(conn, actor_id);
let post = Post::find_by_ap_url(conn, id); let post = Post::find_by_ap_url(conn, id);
let can_delete = actor.and_then(|act| let can_delete = actor
post.clone().map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id)) .and_then(|act| {
).unwrap_or(false); post.clone()
.map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
})
.unwrap_or(false);
if can_delete { if can_delete {
post.map(|p| p.delete(conn)); post.map(|p| p.delete(conn));
} }

View file

@ -1,13 +1,16 @@
use activitypub::activity::{Announce, Undo}; use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}, PUBLIC_VISIBILTY};
use Connection;
use notifications::*; use notifications::*;
use plume_common::activity_pub::{
inbox::{Deletable, FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY,
};
use posts::Post; use posts::Post;
use users::User;
use schema::reshares; use schema::reshares;
use users::User;
use Connection;
#[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)] #[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)]
pub struct Reshare { pub struct Reshare {
@ -15,7 +18,7 @@ pub struct Reshare {
pub user_id: i32, pub user_id: i32,
pub post_id: i32, pub post_id: i32,
pub ap_url: String, pub ap_url: String,
pub creation_date: NaiveDateTime pub creation_date: NaiveDateTime,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -23,29 +26,40 @@ pub struct Reshare {
pub struct NewReshare { pub struct NewReshare {
pub user_id: i32, pub user_id: i32,
pub post_id: i32, pub post_id: i32,
pub ap_url: String pub ap_url: String,
} }
impl Reshare { impl Reshare {
insert!(reshares, NewReshare); insert!(reshares, NewReshare);
get!(reshares); get!(reshares);
find_by!(reshares, find_by_ap_url, ap_url as String); find_by!(reshares, find_by_ap_url, ap_url as String);
find_by!(reshares, find_by_user_on_post, user_id as i32, post_id as i32); find_by!(
reshares,
find_by_user_on_post,
user_id as i32,
post_id as i32
);
pub fn update_ap_url(&self, conn: &Connection) { pub fn update_ap_url(&self, conn: &Connection) {
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(reshares::ap_url.eq(format!( .set(reshares::ap_url.eq(format!(
"{}/reshare/{}", "{}/reshare/{}",
User::get(conn, self.user_id).expect("Reshare::update_ap_url: user error").ap_url, User::get(conn, self.user_id)
Post::get(conn, self.post_id).expect("Reshare::update_ap_url: post error").ap_url .expect("Reshare::update_ap_url: user error")
))) .ap_url,
.execute(conn).expect("Reshare::update_ap_url: update error"); Post::get(conn, self.post_id)
.expect("Reshare::update_ap_url: post error")
.ap_url
)))
.execute(conn)
.expect("Reshare::update_ap_url: update error");
} }
} }
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec<Reshare> { pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec<Reshare> {
reshares::table.filter(reshares::user_id.eq(user.id)) reshares::table
.filter(reshares::user_id.eq(user.id))
.order(reshares::creation_date.desc()) .order(reshares::creation_date.desc())
.limit(limit) .limit(limit)
.load::<Reshare>(conn) .load::<Reshare>(conn)
@ -62,13 +76,29 @@ impl Reshare {
pub fn into_activity(&self, conn: &Connection) -> Announce { pub fn into_activity(&self, conn: &Connection) -> Announce {
let mut act = Announce::default(); let mut act = Announce::default();
act.announce_props.set_actor_link(User::get(conn, self.user_id).expect("Reshare::into_activity: user error").into_id()) act.announce_props
.set_actor_link(
User::get(conn, self.user_id)
.expect("Reshare::into_activity: user error")
.into_id(),
)
.expect("Reshare::into_activity: actor error"); .expect("Reshare::into_activity: actor error");
act.announce_props.set_object_link(Post::get(conn, self.post_id).expect("Reshare::into_activity: post error").into_id()) act.announce_props
.set_object_link(
Post::get(conn, self.post_id)
.expect("Reshare::into_activity: post error")
.into_id(),
)
.expect("Reshare::into_activity: object error"); .expect("Reshare::into_activity: object error");
act.object_props.set_id_string(self.ap_url.clone()).expect("Reshare::into_activity: id error"); act.object_props
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::into_activity: to error"); .set_id_string(self.ap_url.clone())
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Reshare::into_activity: cc error"); .expect("Reshare::into_activity: id error");
act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
.expect("Reshare::into_activity: to error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Reshare::into_activity: cc error");
act act
} }
@ -76,13 +106,33 @@ impl Reshare {
impl FromActivity<Announce, Connection> for Reshare { impl FromActivity<Announce, Connection> for Reshare {
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare { fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare {
let user = User::from_url(conn, announce.announce_props.actor_link::<Id>().expect("Reshare::from_activity: actor error").into()); let user = User::from_url(
let post = Post::find_by_ap_url(conn, announce.announce_props.object_link::<Id>().expect("Reshare::from_activity: object error").into()); conn,
let reshare = Reshare::insert(conn, NewReshare { announce
post_id: post.expect("Reshare::from_activity: post error").id, .announce_props
user_id: user.expect("Reshare::from_activity: user error").id, .actor_link::<Id>()
ap_url: announce.object_props.id_string().unwrap_or(String::from("")) .expect("Reshare::from_activity: actor error")
}); .into(),
);
let post = Post::find_by_ap_url(
conn,
announce
.announce_props
.object_link::<Id>()
.expect("Reshare::from_activity: object error")
.into(),
);
let reshare = Reshare::insert(
conn,
NewReshare {
post_id: post.expect("Reshare::from_activity: post error").id,
user_id: user.expect("Reshare::from_activity: user error").id,
ap_url: announce
.object_props
.id_string()
.unwrap_or(String::from("")),
},
);
reshare.notify(conn); reshare.notify(conn);
reshare reshare
} }
@ -92,30 +142,51 @@ impl Notify<Connection> for Reshare {
fn notify(&self, conn: &Connection) { fn notify(&self, conn: &Connection) {
let post = self.get_post(conn).expect("Reshare::notify: post error"); let post = self.get_post(conn).expect("Reshare::notify: post error");
for author in post.get_authors(conn) { for author in post.get_authors(conn) {
Notification::insert(conn, NewNotification { Notification::insert(
kind: notification_kind::RESHARE.to_string(), conn,
object_id: self.id, NewNotification {
user_id: author.id kind: notification_kind::RESHARE.to_string(),
}); object_id: self.id,
user_id: author.id,
},
);
} }
} }
} }
impl Deletable<Connection, Undo> for Reshare { impl Deletable<Connection, Undo> for Reshare {
fn delete(&self, conn: &Connection) -> Undo { fn delete(&self, conn: &Connection) -> Undo {
diesel::delete(self).execute(conn).expect("Reshare::delete: delete error"); diesel::delete(self)
.execute(conn)
.expect("Reshare::delete: delete error");
// delete associated notification if any // delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(conn).expect("Reshare::delete: notification error"); diesel::delete(&notif)
.execute(conn)
.expect("Reshare::delete: notification error");
} }
let mut act = Undo::default(); let mut act = Undo::default();
act.undo_props.set_actor_link(User::get(conn, self.user_id).expect("Reshare::delete: user error").into_id()).expect("Reshare::delete: actor error"); act.undo_props
act.undo_props.set_object_object(self.into_activity(conn)).expect("Reshare::delete: object error"); .set_actor_link(
act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Reshare::delete: id error"); User::get(conn, self.user_id)
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::delete: to error"); .expect("Reshare::delete: user error")
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Reshare::delete: cc error"); .into_id(),
)
.expect("Reshare::delete: actor error");
act.undo_props
.set_object_object(self.into_activity(conn))
.expect("Reshare::delete: object error");
act.object_props
.set_id_string(format!("{}#delete", self.ap_url))
.expect("Reshare::delete: id error");
act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
.expect("Reshare::delete: to error");
act.object_props
.set_cc_link_vec::<Id>(vec![])
.expect("Reshare::delete: cc error");
act act
} }

View file

@ -1,13 +1,19 @@
use ammonia::{Builder, UrlRelative}; use ammonia::{Builder, UrlRelative};
use serde::{self, Serialize, Deserialize, use diesel::{
Serializer, Deserializer, de::Visitor}; self,
use std::{fmt::{self, Display}, deserialize::Queryable,
borrow::{Borrow, Cow}, io::Write, serialize::{self, Output},
iter, ops::Deref};
use diesel::{self, deserialize::Queryable,
types::ToSql,
sql_types::Text, sql_types::Text,
serialize::{self, Output}}; types::ToSql,
};
use serde::{self, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::{
borrow::{Borrow, Cow},
fmt::{self, Display},
io::Write,
iter,
ops::Deref,
};
lazy_static! { lazy_static! {
static ref CLEAN: Builder<'static> = { static ref CLEAN: Builder<'static> = {
@ -16,17 +22,18 @@ lazy_static! {
.add_tags(iter::once("iframe")) .add_tags(iter::once("iframe"))
.id_prefix(Some("postcontent-")) .id_prefix(Some("postcontent-"))
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix))) .url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
.add_tag_attributes("iframe", .add_tag_attributes(
["width", "height", "src", "frameborder"] "iframe",
.iter() ["width", "height", "src", "frameborder"].iter().map(|&v| v),
.map(|&v| v)); );
b b
}; };
} }
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 # if url.starts_with('#') && !url.starts_with("#postcontent-") {
let mut new_url = "#postcontent-".to_owned();//change to valid id //if start with an #
let mut new_url = "#postcontent-".to_owned(); //change to valid id
new_url.push_str(&url[1..]); new_url.push_str(&url[1..]);
Some(Cow::Owned(new_url)) Some(Cow::Owned(new_url))
} else { } else {
@ -34,15 +41,15 @@ fn url_add_prefix(url: &str) -> Option<Cow<str>> {
} }
} }
#[derive(Debug, Clone, AsExpression, FromSqlRow, Default)] #[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
#[sql_type = "Text"] #[sql_type = "Text"]
pub struct SafeString{ pub struct SafeString {
value: String, value: String,
} }
impl SafeString{ impl SafeString {
pub fn new(value: &str) -> Self { pub fn new(value: &str) -> Self {
SafeString{ SafeString {
value: CLEAN.clean(&value).to_string(), value: CLEAN.clean(&value).to_string(),
} }
} }
@ -56,7 +63,9 @@ pub fn new(value: &str) -> Self {
impl Serialize for SafeString { impl Serialize for SafeString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer, { where
S: Serializer,
{
serializer.serialize_str(&self.value) serializer.serialize_str(&self.value)
} }
} }
@ -66,22 +75,24 @@ struct SafeStringVisitor;
impl<'de> Visitor<'de> for SafeStringVisitor { impl<'de> Visitor<'de> for SafeStringVisitor {
type Value = SafeString; type Value = SafeString;
fn expecting(&self, formatter:&mut fmt::Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string") formatter.write_str("a string")
} }
fn visit_str<E>(self, value: &str) -> Result<SafeString, E> fn visit_str<E>(self, value: &str) -> Result<SafeString, E>
where E: serde::de::Error{ where
E: serde::de::Error,
{
Ok(SafeString::new(value)) Ok(SafeString::new(value))
} }
} }
impl<'de> Deserialize<'de> for SafeString { impl<'de> Deserialize<'de> for SafeString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>, { where
Ok( D: Deserializer<'de>,
deserializer.deserialize_string(SafeStringVisitor)? {
) Ok(deserializer.deserialize_string(SafeStringVisitor)?)
} }
} }
@ -101,17 +112,16 @@ impl Queryable<Text, diesel::sqlite::Sqlite> for SafeString {
} }
} }
impl<DB> ToSql<diesel::sql_types::Text, DB> for SafeString impl<DB> ToSql<diesel::sql_types::Text, DB> for SafeString
where where
DB: diesel::backend::Backend, DB: diesel::backend::Backend,
str: ToSql<diesel::sql_types::Text, DB>, { str: ToSql<diesel::sql_types::Text, DB>,
{
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result { fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
str::to_sql(&self.value, out) str::to_sql(&self.value, out)
} }
} }
impl Borrow<str> for SafeString { impl Borrow<str> for SafeString {
fn borrow(&self) -> &str { fn borrow(&self) -> &str {
&self.value &self.value
@ -137,8 +147,8 @@ impl AsRef<str> for SafeString {
} }
} }
use rocket::request::FromFormValue;
use rocket::http::RawStr; use rocket::http::RawStr;
use rocket::request::FromFormValue;
impl<'v> FromFormValue<'v> for SafeString { impl<'v> FromFormValue<'v> for SafeString {
type Error = &'v RawStr; type Error = &'v RawStr;

View file

@ -1,16 +1,16 @@
use diesel::{self, ExpressionMethods, RunQueryDsl, QueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::Hashtag;
use {ap_url, Connection};
use instance::Instance; use instance::Instance;
use plume_common::activity_pub::Hashtag;
use schema::tags; use schema::tags;
use {ap_url, Connection};
#[derive(Clone, Identifiable, Serialize, Queryable)] #[derive(Clone, Identifiable, Serialize, Queryable)]
pub struct Tag { pub struct Tag {
pub id: i32, pub id: i32,
pub tag: String, pub tag: String,
pub is_hashtag: bool, pub is_hashtag: bool,
pub post_id: i32 pub post_id: i32,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -18,7 +18,7 @@ pub struct Tag {
pub struct NewTag { pub struct NewTag {
pub tag: String, pub tag: String,
pub is_hashtag: bool, pub is_hashtag: bool,
pub post_id: i32 pub post_id: i32,
} }
impl Tag { impl Tag {
@ -29,33 +29,46 @@ impl Tag {
pub fn into_activity(&self, conn: &Connection) -> Hashtag { pub fn into_activity(&self, conn: &Connection) -> Hashtag {
let mut ht = Hashtag::default(); let mut ht = Hashtag::default();
ht.set_href_string(ap_url(format!("{}/tag/{}", ht.set_href_string(ap_url(format!(
Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain, "{}/tag/{}",
self.tag) Instance::get_local(conn)
)).expect("Tag::into_activity: href error"); .expect("Tag::into_activity: local instance not found error")
ht.set_name_string(self.tag.clone()).expect("Tag::into_activity: name error"); .public_domain,
self.tag
))).expect("Tag::into_activity: href error");
ht.set_name_string(self.tag.clone())
.expect("Tag::into_activity: name error");
ht ht
} }
pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag { pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag {
Tag::insert(conn, NewTag { Tag::insert(
tag: tag.name_string().expect("Tag::from_activity: name error"), conn,
is_hashtag, NewTag {
post_id: post tag: tag.name_string().expect("Tag::from_activity: name error"),
}) is_hashtag,
post_id: post,
},
)
} }
pub fn build_activity(conn: &Connection, tag: String) -> Hashtag { pub fn build_activity(conn: &Connection, tag: String) -> Hashtag {
let mut ht = Hashtag::default(); let mut ht = Hashtag::default();
ht.set_href_string(ap_url(format!("{}/tag/{}", ht.set_href_string(ap_url(format!(
Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain, "{}/tag/{}",
tag) Instance::get_local(conn)
)).expect("Tag::into_activity: href error"); .expect("Tag::into_activity: local instance not found error")
ht.set_name_string(tag).expect("Tag::into_activity: name error"); .public_domain,
tag
))).expect("Tag::into_activity: href error");
ht.set_name_string(tag)
.expect("Tag::into_activity: name error");
ht ht
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) {
diesel::delete(self).execute(conn).expect("Tag::delete: database error"); diesel::delete(self)
.execute(conn)
.expect("Tag::delete: database error");
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,11 @@
extern crate diesel; extern crate diesel;
#[macro_use] extern crate diesel_migrations; #[macro_use]
extern crate diesel_migrations;
extern crate plume_models; extern crate plume_models;
use diesel::Connection; use diesel::Connection;
use plume_models::{ use plume_models::{Connection as Conn, DATABASE_URL};
DATABASE_URL,
Connection as Conn,
instance::*,
safe_string::SafeString,
};
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
embed_migrations!("../migrations/sqlite"); embed_migrations!("../migrations/sqlite");
@ -24,24 +20,7 @@ fn db() -> Conn {
} }
#[test] #[test]
fn instance_insert() { fn empty_test() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| Ok(()));
Instance::insert(conn, NewInstance {
default_license: "WTFPL".to_string(),
local: true,
long_description: SafeString::new("This is my instance."),
long_description_html: "<p>This is my instance</p>".to_string(),
short_description: SafeString::new("My instance."),
short_description_html: "<p>My instance</p>".to_string(),
name: "My instance".to_string(),
open_registrations: true,
public_domain: "plu.me".to_string(),
});
let inst = Instance::get_local(conn);
assert!(inst.is_some());
let inst = inst.unwrap();
assert_eq!(inst.name, "My instance".to_string());
Ok(())
});
} }

View file

@ -13,76 +13,112 @@ use activitypub::{
use failure::Error; use failure::Error;
use serde_json; use serde_json;
use plume_common::activity_pub::{Id, inbox::{Deletable, FromActivity, InboxError}}; use plume_common::activity_pub::{
inbox::{Deletable, FromActivity, InboxError},
Id,
};
use plume_models::{ use plume_models::{
Connection, comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
comments::Comment, users::User, Connection,
follows::Follow,
instance::Instance,
likes,
reshares::Reshare,
posts::Post,
users::User
}; };
pub trait Inbox { pub trait Inbox {
fn received(&self, conn: &Connection, act: serde_json::Value) -> Result<(), Error> { fn received(&self, conn: &Connection, act: serde_json::Value) -> Result<(), Error> {
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| act["actor"]["id"].as_str().expect("Inbox::received: actor_id missing error"))); let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| {
act["actor"]["id"]
.as_str()
.expect("Inbox::received: actor_id missing error")
}));
match act["type"].as_str() { match act["type"].as_str() {
Some(t) => { Some(t) => match t {
match t { "Announce" => {
"Announce" => { Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); Ok(())
Ok(())
},
"Create" => {
let act: Create = serde_json::from_value(act.clone())?;
if Post::try_from_activity(conn, act.clone()) || Comment::try_from_activity(conn, act) {
Ok(())
} else {
Err(InboxError::InvalidType)?
}
},
"Delete" => {
let act: Delete = serde_json::from_value(act.clone())?;
Post::delete_id(act.delete_props.object_object::<Tombstone>()?.object_props.id_string()?, actor_id.into(), conn);
Ok(())
},
"Follow" => {
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
Ok(())
},
"Like" => {
likes::Like::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
Ok(())
},
"Undo" => {
let act: Undo = serde_json::from_value(act.clone())?;
match act.undo_props.object["type"].as_str().expect("Inbox::received: undo without original type error") {
"Like" => {
likes::Like::delete_id(act.undo_props.object_object::<Like>()?.object_props.id_string()?, actor_id.into(), conn);
Ok(())
},
"Announce" => {
Reshare::delete_id(act.undo_props.object_object::<Announce>()?.object_props.id_string()?, actor_id.into(), conn);
Ok(())
},
"Follow" => {
Follow::delete_id(act.undo_props.object_object::<FollowAct>()?.object_props.id_string()?, actor_id.into(), conn);
Ok(())
}
_ => Err(InboxError::CantUndo)?
}
}
"Update" => {
let act: Update = serde_json::from_value(act.clone())?;
Post::handle_update(conn, act.update_props.object_object()?);
Ok(())
}
_ => Err(InboxError::InvalidType)?
} }
"Create" => {
let act: Create = serde_json::from_value(act.clone())?;
if Post::try_from_activity(conn, act.clone())
|| Comment::try_from_activity(conn, act)
{
Ok(())
} else {
Err(InboxError::InvalidType)?
}
}
"Delete" => {
let act: Delete = serde_json::from_value(act.clone())?;
Post::delete_id(
act.delete_props
.object_object::<Tombstone>()?
.object_props
.id_string()?,
actor_id.into(),
conn,
);
Ok(())
}
"Follow" => {
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
Ok(())
}
"Like" => {
likes::Like::from_activity(
conn,
serde_json::from_value(act.clone())?,
actor_id,
);
Ok(())
}
"Undo" => {
let act: Undo = serde_json::from_value(act.clone())?;
match act.undo_props.object["type"]
.as_str()
.expect("Inbox::received: undo without original type error")
{
"Like" => {
likes::Like::delete_id(
act.undo_props
.object_object::<Like>()?
.object_props
.id_string()?,
actor_id.into(),
conn,
);
Ok(())
}
"Announce" => {
Reshare::delete_id(
act.undo_props
.object_object::<Announce>()?
.object_props
.id_string()?,
actor_id.into(),
conn,
);
Ok(())
}
"Follow" => {
Follow::delete_id(
act.undo_props
.object_object::<FollowAct>()?
.object_props
.id_string()?,
actor_id.into(),
conn,
);
Ok(())
}
_ => Err(InboxError::CantUndo)?,
}
}
"Update" => {
let act: Update = serde_json::from_value(act.clone())?;
Post::handle_update(conn, act.update_props.object_object()?);
Ok(())
}
_ => Err(InboxError::InvalidType)?,
}, },
None => Err(InboxError::NoType)? None => Err(InboxError::NoType)?,
} }
} }
} }

View file

@ -1,36 +1,27 @@
use activitypub::{ use activitypub::{activity::Create, collection::OrderedCollection, object::Article};
activity::Create,
collection::OrderedCollection,
object::Article
};
use atom_syndication::{Entry, FeedBuilder}; use atom_syndication::{Entry, FeedBuilder};
use rocket::{ use rocket::{
http::{ContentType, Cookies},
request::LenientForm, request::LenientForm,
response::{Content, Flash, Redirect, status}, response::{status, Content, Flash, Redirect},
http::{ContentType, Cookies}
}; };
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use validator::{Validate, ValidationError}; use validator::{Validate, ValidationError};
use workerpool::thunk::*; use workerpool::thunk::*;
use inbox::Inbox;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId, ApRequest, broadcast,
inbox::{FromActivity, Notify, Deletable}, inbox::{Deletable, FromActivity, Notify},
sign::{Signable, verify_http_headers} sign::{verify_http_headers, Signable},
ActivityStream, ApRequest, Id, IntoId,
}; };
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blogs::Blog, blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::Post,
db_conn::DbConn, reshares::Reshare, users::*,
follows,
headers::Headers,
instance::Instance,
posts::Post,
reshares::Reshare,
users::*
}; };
use inbox::Inbox;
use routes::Page; use routes::Page;
use Worker; use Worker;
@ -38,57 +29,84 @@ use Worker;
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> { fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
match user { match user {
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))), Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
None => Err(utils::requires_login("", uri!(me).into())) None => Err(utils::requires_login("", uri!(me).into())),
} }
} }
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fecth_articles_conn: DbConn, fecth_followers_conn: DbConn, update_conn: DbConn) -> Template { fn details(
may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| { name: String,
let recents = Post::get_recents_for_author(&*conn, &user, 6); conn: DbConn,
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); account: Option<User>,
let user_id = user.id.clone(); worker: Worker,
let n_followers = user.get_followers(&*conn).len(); fecth_articles_conn: DbConn,
fecth_followers_conn: DbConn,
update_conn: DbConn,
) -> Template {
may_fail!(
account.map(|a| a.to_json(&*conn)),
User::find_by_fqn(&*conn, name),
"Couldn't find requested user",
|user| {
let recents = Post::get_recents_for_author(&*conn, &user, 6);
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
let user_id = user.id.clone();
let n_followers = user.get_followers(&*conn).len();
if !user.get_instance(&*conn).local { if !user.get_instance(&*conn).local {
// Fetch new articles // Fetch new articles
let user_clone = user.clone(); let user_clone = user.clone();
worker.execute(Thunk::of(move || {
for create_act in user_clone.fetch_outbox::<Create>() {
match create_act.create_props.object_object::<Article>() {
Ok(article) => {
Post::from_activity(&*fecth_articles_conn, article, user_clone.clone().into_id());
println!("Fetched article from remote user");
}
Err(e) => println!("Error while fetching articles in background: {:?}", e)
}
}
}));
// Fetch followers
let user_clone = user.clone();
worker.execute(Thunk::of(move || {
for user_id in user_clone.fetch_followers_ids() {
let follower = User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
.unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("user::details: Couldn't fetch follower"));
follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow {
follower_id: follower.id,
following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
});
}
}));
// Update profile information if needed
let user_clone = user.clone();
if user.needs_update() {
worker.execute(Thunk::of(move || { worker.execute(Thunk::of(move || {
user_clone.refetch(&*update_conn); for create_act in user_clone.fetch_outbox::<Create>() {
})) match create_act.create_props.object_object::<Article>() {
} Ok(article) => {
} Post::from_activity(
&*fecth_articles_conn,
article,
user_clone.clone().into_id(),
);
println!("Fetched article from remote user");
}
Err(e) => {
println!("Error while fetching articles in background: {:?}", e)
}
}
}
}));
Template::render("users/details", json!({ // Fetch followers
let user_clone = user.clone();
worker.execute(Thunk::of(move || {
for user_id in user_clone.fetch_followers_ids() {
let follower =
User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
.unwrap_or_else(|| {
User::fetch_from_url(&*fecth_followers_conn, user_id)
.expect("user::details: Couldn't fetch follower")
});
follows::Follow::insert(
&*fecth_followers_conn,
follows::NewFollow {
follower_id: follower.id,
following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
},
);
}
}));
// Update profile information if needed
let user_clone = user.clone();
if user.needs_update() {
worker.execute(Thunk::of(move || {
user_clone.refetch(&*update_conn);
}))
}
}
Template::render(
"users/details",
json!({
"user": user.to_json(&*conn), "user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain, "instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn), "is_remote": user.instance_id != Instance::local_id(&*conn),
@ -98,25 +116,30 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
"reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(), "reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"is_self": account.map(|a| a.id == user_id).unwrap_or(false), "is_self": account.map(|a| a.id == user_id).unwrap_or(false),
"n_followers": n_followers "n_followers": n_followers
})) }),
}) )
}
)
} }
#[get("/dashboard")] #[get("/dashboard")]
fn dashboard(user: User, conn: DbConn) -> Template { fn dashboard(user: User, conn: DbConn) -> Template {
let blogs = Blog::find_for_author(&*conn, user.id); let blogs = Blog::find_for_author(&*conn, &user);
Template::render("users/dashboard", json!({ Template::render(
"users/dashboard",
json!({
"account": user.to_json(&*conn), "account": user.to_json(&*conn),
"blogs": blogs, "blogs": blogs,
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), "drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
})) }),
)
} }
#[get("/dashboard", rank = 2)] #[get("/dashboard", rank = 2)]
fn dashboard_auth() -> Flash<Redirect> { fn dashboard_auth() -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to access your dashboard", "You need to be logged in order to access your dashboard",
uri!(dashboard).into() uri!(dashboard).into(),
) )
} }
@ -125,13 +148,18 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redi
let target = User::find_by_fqn(&*conn, name.clone())?; let target = User::find_by_fqn(&*conn, name.clone())?;
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) { if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.delete(&*conn); let delete_act = follow.delete(&*conn);
worker.execute(Thunk::of(move || broadcast(&user, delete_act, vec![target]))); worker.execute(Thunk::of(move || {
broadcast(&user, delete_act, vec![target])
}));
} else { } else {
let f = follows::Follow::insert(&*conn, follows::NewFollow { let f = follows::Follow::insert(
follower_id: user.id, &*conn,
following_id: target.id, follows::NewFollow {
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url), follower_id: user.id,
}); following_id: target.id,
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
},
);
f.notify(&*conn); f.notify(&*conn);
let act = f.into_activity(&*conn); let act = f.into_activity(&*conn);
@ -144,17 +172,23 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redi
fn follow_auth(name: String) -> Flash<Redirect> { fn follow_auth(name: String) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to follow someone", "You need to be logged in order to follow someone",
uri!(follow: name = name).into() uri!(follow: name = name).into(),
) )
} }
#[get("/@/<name>/followers?<page>")] #[get("/@/<name>/followers?<page>")]
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template { fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template {
may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name.clone()), "Couldn't find requested user", |user| { may_fail!(
let user_id = user.id.clone(); account.map(|a| a.to_json(&*conn)),
let followers_count = user.get_followers(&*conn).len(); User::find_by_fqn(&*conn, name.clone()),
"Couldn't find requested user",
|user| {
let user_id = user.id.clone();
let followers_count = user.get_followers(&*conn).len();
Template::render("users/followers", json!({ Template::render(
"users/followers",
json!({
"user": user.to_json(&*conn), "user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain, "instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn), "is_remote": user.instance_id != Instance::local_id(&*conn),
@ -165,8 +199,10 @@ fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page:
"n_followers": followers_count, "n_followers": followers_count,
"page": page.page, "page": page.page,
"n_pages": Page::total(followers_count as i32) "n_pages": Page::total(followers_count as i32)
})) }),
}) )
}
)
} }
#[get("/@/<name>/followers", rank = 2)] #[get("/@/<name>/followers", rank = 2)]
@ -174,29 +210,38 @@ fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
followers_paginated(name, conn, account, Page::first()) followers_paginated(name, conn, account, Page::first())
} }
#[get("/@/<name>", rank = 1)] #[get("/@/<name>", rank = 1)]
fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomPerson>> { fn activity_details(
name: String,
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<CustomPerson>> {
let user = User::find_local(&*conn, name)?; let user = User::find_local(&*conn, name)?;
Some(ActivityStream::new(user.into_activity(&*conn))) Some(ActivityStream::new(user.into_activity(&*conn)))
} }
#[get("/users/new")] #[get("/users/new")]
fn new(user: Option<User>, conn: DbConn) -> Template { fn new(user: Option<User>, conn: DbConn) -> Template {
Template::render("users/new", json!({ Template::render(
"users/new",
json!({
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), "enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
"account": user.map(|u| u.to_json(&*conn)), "account": user.map(|u| u.to_json(&*conn)),
"errors": null, "errors": null,
"form": null "form": null
})) }),
)
} }
#[get("/@/<name>/edit")] #[get("/@/<name>/edit")]
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> { fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
if user.username == name && !name.contains("@") { if user.username == name && !name.contains("@") {
Some(Template::render("users/edit", json!({ Some(Template::render(
"users/edit",
json!({
"account": user.to_json(&*conn) "account": user.to_json(&*conn)
}))) }),
))
} else { } else {
None None
} }
@ -206,7 +251,7 @@ fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
fn edit_auth(name: String) -> Flash<Redirect> { fn edit_auth(name: String) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
"You need to be logged in order to edit your profile", "You need to be logged in order to edit your profile",
uri!(edit: name = name).into() uri!(edit: name = name).into(),
) )
} }
@ -219,10 +264,22 @@ struct UpdateUserForm {
#[put("/@/<_name>/edit", data = "<data>")] #[put("/@/<_name>/edit", data = "<data>")]
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect { fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
user.update(&*conn, user.update(
data.get().display_name.clone().unwrap_or(user.display_name.to_string()).to_string(), &*conn,
data.get().email.clone().unwrap_or(user.email.clone().unwrap()).to_string(), data.get()
data.get().summary.clone().unwrap_or(user.summary.to_string()) .display_name
.clone()
.unwrap_or(user.display_name.to_string())
.to_string(),
data.get()
.email
.clone()
.unwrap_or(user.email.clone().unwrap())
.to_string(),
data.get()
.summary
.clone()
.unwrap_or(user.summary.to_string()),
); );
Redirect::to(uri!(me)) Redirect::to(uri!(me))
} }
@ -233,7 +290,9 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
if user.id == account.id { if user.id == account.id {
account.delete(&*conn); account.delete(&*conn);
cookies.get_private(AUTH_COOKIE).map(|cookie| cookies.remove_private(cookie)); cookies
.get_private(AUTH_COOKIE)
.map(|cookie| cookies.remove_private(cookie));
Some(Redirect::to(uri!(super::instance::index))) Some(Redirect::to(uri!(super::instance::index)))
} else { } else {
@ -242,16 +301,32 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
} }
#[derive(FromForm, Serialize, Validate)] #[derive(FromForm, Serialize, Validate)]
#[validate(schema(function = "passwords_match", skip_on_field_errors = "false", message = "Passwords are not matching"))] #[validate(
schema(
function = "passwords_match",
skip_on_field_errors = "false",
message = "Passwords are not matching"
)
)]
struct NewUserForm { struct NewUserForm {
#[validate(length(min = "1", message = "Username can't be empty"))] #[validate(length(min = "1", message = "Username can't be empty"))]
username: String, username: String,
#[validate(email(message = "Invalid email"))] #[validate(email(message = "Invalid email"))]
email: String, 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"
)
)]
password: String, password: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))] #[validate(
password_confirmation: String length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
password_confirmation: String,
} }
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> { fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
@ -264,29 +339,37 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
#[post("/users/new", data = "<data>")] #[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> { fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
if !Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true) { if !Instance::get_local(&*conn)
.map(|i| i.open_registrations)
.unwrap_or(true)
{
return Ok(Redirect::to(uri!(new))); // Actually, it is an error return Ok(Redirect::to(uri!(new))); // Actually, it is an error
} }
let form = data.get(); let form = data.get();
form.validate() form.validate()
.map(|_| { .map(|_| {
NewUser::new_local( NewUser::new_local(
&*conn, &*conn,
form.username.to_string(), form.username.to_string(),
form.username.to_string(), form.username.to_string(),
false, false,
String::from(""), String::from(""),
form.email.to_string(), form.email.to_string(),
User::hash_pass(form.password.to_string()) User::hash_pass(form.password.to_string()),
).update_boxes(&*conn); ).update_boxes(&*conn);
Redirect::to(uri!(super::session::new)) Redirect::to(uri!(super::session::new))
}) })
.map_err(|e| Template::render("users/new", json!({ .map_err(|e| {
Template::render(
"users/new",
json!({
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), "enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
"errors": e.inner(), "errors": e.inner(),
"form": form "form": form
}))) }),
)
})
} }
#[get("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
@ -296,18 +379,32 @@ fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection
} }
#[post("/@/<name>/inbox", data = "<data>")] #[post("/@/<name>/inbox", data = "<data>")]
fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> Result<String, Option<status::BadRequest<&'static str>>> { fn inbox(
name: String,
conn: DbConn,
data: String,
headers: Headers,
) -> Result<String, Option<status::BadRequest<&'static str>>> {
let user = User::find_local(&*conn, name).ok_or(None)?; let user = User::find_local(&*conn, name).ok_or(None)?;
let act: serde_json::Value = serde_json::from_str(&data[..]).expect("user::inbox: deserialization error"); let act: serde_json::Value =
serde_json::from_str(&data[..]).expect("user::inbox: deserialization error");
let activity = act.clone(); let activity = act.clone();
let actor_id = activity["actor"].as_str() let actor_id = activity["actor"]
.or_else(|| activity["actor"]["id"].as_str()).ok_or(Some(status::BadRequest(Some("Missing actor id for activity"))))?; .as_str()
.or_else(|| activity["actor"]["id"].as_str())
.ok_or(Some(status::BadRequest(Some(
"Missing actor id for activity",
))))?;
let actor = User::from_url(&conn, actor_id.to_owned()).expect("user::inbox: user error"); let actor = User::from_url(&conn, actor_id.to_owned()).expect("user::inbox: user error");
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure() && if !verify_http_headers(&actor, headers.0.clone(), data).is_secure()
!act.clone().verify(&actor) { && !act.clone().verify(&actor)
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); {
println!(
"Rejected invalid activity supposedly from {}, with headers {:?}",
actor.username, headers.0
);
return Err(Some(status::BadRequest(Some("Invalid signature")))); return Err(Some(status::BadRequest(Some("Invalid signature"))));
} }
@ -324,14 +421,28 @@ fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> Result<S
} }
#[get("/@/<name>/followers")] #[get("/@/<name>/followers")]
fn ap_followers(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<OrderedCollection>> { fn ap_followers(
name: String,
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_local(&*conn, name)?; let user = User::find_local(&*conn, name)?;
let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::<Vec<Id>>(); let followers = user
.get_followers(&*conn)
.into_iter()
.map(|f| Id::new(f.ap_url))
.collect::<Vec<Id>>();
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.object_props.set_id_string(user.followers_endpoint).expect("user::ap_followers: id error"); coll.object_props
coll.collection_props.set_total_items_u64(followers.len() as u64).expect("user::ap_followers: totalItems error"); .set_id_string(user.followers_endpoint)
coll.collection_props.set_items_link_vec(followers).expect("user::ap_followers items error"); .expect("user::ap_followers: id error");
coll.collection_props
.set_total_items_u64(followers.len() as u64)
.expect("user::ap_followers: totalItems error");
coll.collection_props
.set_items_link_vec(followers)
.expect("user::ap_followers items error");
Some(ActivityStream::new(coll)) Some(ActivityStream::new(coll))
} }
@ -340,12 +451,19 @@ fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let author = User::find_by_fqn(&*conn, name.clone())?; let author = User::find_by_fqn(&*conn, name.clone())?;
let feed = FeedBuilder::default() let feed = FeedBuilder::default()
.title(author.display_name.clone()) .title(author.display_name.clone())
.id(Instance::get_local(&*conn).unwrap().compute_box("~", name, "atom.xml")) .id(Instance::get_local(&*conn)
.entries(Post::get_recents_for_author(&*conn, &author, 15) .unwrap()
.into_iter() .compute_box("~", name, "atom.xml"))
.map(|p| super::post_to_atom(p, &*conn)) .entries(
.collect::<Vec<Entry>>()) Post::get_recents_for_author(&*conn, &author, 15)
.into_iter()
.map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>(),
)
.build() .build()
.expect("user::atom_feed: Error building Atom feed"); .expect("user::atom_feed: Error building Atom feed");
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string())) Some(Content(
ContentType::new("application", "atom+xml"),
feed.to_string(),
))
} }