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"
env:
- 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
script:
- |
@ -49,7 +49,7 @@ jobs:
name: "Test with Sqlite backend"
env:
- 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:
- |
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;
@ -9,7 +9,7 @@ pub enum InboxError {
#[fail(display = "Invalid activity type")]
InvalidType,
#[fail(display = "Couldn't undo activity")]
CantUndo
CantUndo,
}
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 {
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
} else {
false
@ -32,7 +38,6 @@ pub trait Notify<C> {
pub trait Deletable<C, A> {
fn delete(&self, conn: &C) -> A;
fn delete_id(id: String, actor_id: String, conn: &C);
}
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 reqwest::Client;
use rocket::{
Outcome, http::Status,
response::{Response, Responder},
request::{FromRequest, Request}
http::Status,
request::{FromRequest, Request},
response::{Responder, Response},
Outcome,
};
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/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> {
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> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context();
serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r)
serde_json::to_string(&json).respond_to(request).map(|r| {
Response::build_from(r)
.raw_header("Content-Type", "application/activity+json")
.finalize())
.finalize()
})
}
}
@ -76,29 +79,45 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
type 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
.headers()
.get_one("Accept")
.map(|header| {
header
.split(",")
.map(|ct| match ct.trim() {
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" |
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" |
"application/activity+json" |
"application/ld+json" => Outcome::Success(ApRequest),
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
| "application/activity+json"
| "application/ld+json" => Outcome::Success(ApRequest),
"text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false)
}).fold(Outcome::Forward(false), |out, ct| if out.clone().forwarded().unwrap_or(out.is_success()) {
_ => 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(()))
}
})
.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>) {
let boxes = to.into_iter()
pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(
sender: &S,
act: A,
to: Vec<T>,
) {
let boxes = to
.into_iter()
.filter(|u| !u.is_local())
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.collect::<Vec<String>>()
.unique();
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
act["@context"] = context();
let signed = act.sign(sender);
@ -121,8 +140,8 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(send
} else {
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")]
pub struct ApSignature {
#[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)]
@ -165,7 +184,7 @@ pub struct PublicKey {
pub owner: Option<serde_json::Value>,
#[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)]

View file

@ -1,5 +1,5 @@
use base64;
use chrono::{DateTime, offset::Utc};
use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT};
use std::ops::Deref;
@ -14,35 +14,52 @@ pub struct Digest(String);
impl Digest {
pub fn digest(body: String) -> HeaderValue {
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
hasher.update(&body.into_bytes()[..]).expect("Digest::digest: content insertion error");
let mut hasher =
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"));
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 {
if self.algorithm()=="SHA-256" {
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
hasher.update(&body.into_bytes()).expect("Digest::digest: content insertion error");
self.value().deref()==hasher.finish().expect("Digest::digest: finalizing error").deref()
if self.algorithm() == "SHA-256" {
let mut hasher =
Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
hasher
.update(&body.into_bytes())
.expect("Digest::digest: content insertion error");
self.value().deref()
== hasher
.finish()
.expect("Digest::digest: finalizing error")
.deref()
} else {
false //algorithm not supported
}
}
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]
}
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")
}
pub fn from_header(dig: &str) -> Result<Self, ()> {
if let Some(pos) = dig.find('=') {
let pos = pos+1;
let pos = pos + 1;
if let Ok(_) = base64::decode(&dig[pos..]) {
Ok(Digest(dig.to_owned()))
} else {
@ -60,15 +77,42 @@ pub fn headers() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT));
headers.insert(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(
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
}
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_headers = headers.iter().map(|(h,_)| h.as_str()).collect::<Vec<&str>>().join(" ").to_lowercase();
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_headers = headers
.iter()
.map(|(h, _)| h.as_str())
.collect::<Vec<&str>>()
.join(" ")
.to_lowercase();
let data = signer.sign(signed_string);
let sign = base64::encode(&data[..]);

View file

@ -1,12 +1,8 @@
use super::request;
use base64;
use chrono::Utc;
use hex;
use openssl::{
pkey::PKey,
rsa::Rsa,
sha::sha256
};
use super::request;
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
use rocket::http::HeaderMap;
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 = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error");
(
keypair.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")
keypair
.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 {
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
fn verify<T>(self, creator: &T) -> bool where T: Signer;
fn sign<T>(&mut self, creator: &T) -> &mut Self
where
T: Signer;
fn verify<T>(self, creator: &T) -> bool
where
T: Signer;
fn hash(data: String) -> String {
let bytes = data.into_bytes();
@ -48,10 +52,12 @@ impl Signable for serde_json::Value {
"created": creation_date
});
let options_hash = Self::hash(json!({
let options_hash = Self::hash(
json!({
"@context": "https://w3id.org/identity/v1",
"created": creation_date
}).to_string());
}).to_string(),
);
let document_hash = Self::hash(self.to_string());
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 {
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
return false;
};
let signature = if let Ok(sig) = base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) {
let signature = if let Ok(sig) =
base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or(""))
{
sig
} else {
return false
return false;
};
let creation_date = &signature_obj["created"];
let options_hash = Self::hash(json!({
let options_hash = Self::hash(
json!({
"@context": "https://w3id.org/identity/v1",
"created": creation_date
}).to_string());
}).to_string(),
);
let document_hash = Self::hash(self.to_string());
let to_be_signed = options_hash + &document_hash;
creator.verify(to_be_signed, signature)
}
}
#[derive(Debug,Copy,Clone,PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum SignatureValidity {
Invalid,
ValidNoDigest,
@ -95,14 +106,18 @@ pub enum SignatureValidity {
impl SignatureValidity {
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");
if sig_header.is_none() {
return SignatureValidity::Absent
return SignatureValidity::Absent;
}
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;
for part in sig_header.split(',') {
match part {
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("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("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("headers=") => headers = Some(&part[9..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
return SignatureValidity::Invalid
if signature.is_none() || headers.is_none() {
//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 h = headers.iter()
.map(|header| (header,all_headers.get_one(header)))
let h = headers
.iter()
.map(|header| (header, all_headers.get_one(header)))
.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())) {
return SignatureValidity::Invalid
return SignatureValidity::Invalid;
}
if !headers.contains(&"digest") {// signature is valid, but body content is not verified
return SignatureValidity::ValidNoDigest
if !headers.contains(&"digest") {
// signature is valid, but body content is not verified
return SignatureValidity::ValidNoDigest;
}
let digest = all_headers.get_one("digest").unwrap_or("");
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
} 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;

View file

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

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

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

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 reqwest::{Client,
header::{ACCEPT, HeaderValue}
};
use serde_json;
use url::Url;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use openssl::{
hash::MessageDigest,
pkey::{PKey, Private},
rsa::Rsa,
sign::{Signer,Verifier}
sign::{Signer, Verifier},
};
use reqwest::{
header::{HeaderValue, ACCEPT},
Client,
};
use serde_json;
use url::Url;
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 plume_common::activity_pub::{
ap_accept_header,
inbox::{Deletable, WithInbox},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey,
};
use posts::Post;
use safe_string::SafeString;
use schema::blogs;
use users::User;
use {Connection, BASE_URL, USE_HTTPS};
pub type CustomGroup = CustomObject<ApSignature, Group>;
@ -40,7 +41,7 @@ pub struct Blog {
pub creation_date: NaiveDateTime,
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String
pub public_key: String,
}
#[derive(Insertable)]
@ -54,7 +55,7 @@ pub struct NewBlog {
pub instance_id: i32,
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String
pub public_key: String,
}
const BLOG_PREFIX: &'static str = "~";
@ -72,16 +73,22 @@ impl Blog {
pub fn list_authors(&self, conn: &Connection) -> Vec<User> {
use schema::blog_authors;
use schema::users;
let authors_ids = blog_authors::table.filter(blog_authors::blog_id.eq(self.id)).select(blog_authors::author_id);
users::table.filter(users::id.eq_any(authors_ids))
let authors_ids = blog_authors::table
.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)
.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;
let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id);
blogs::table.filter(blogs::id.eq_any(author_ids))
let author_ids = blog_authors::table
.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)
.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> {
if fqn.contains("@") { // remote blog
match Instance::find_by_domain(conn, String::from(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) {
if fqn.contains("@") {
// remote blog
match Instance::find_by_domain(
conn,
String::from(
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)
None => Blog::fetch_from_webfinger(conn, fqn),
}
} else { // local blog
} else {
// local blog
Blog::find_local(conn, fqn)
}
}
fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option<Blog> {
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) => {
println!("{:?}", details);
None
@ -119,17 +151,37 @@ impl Blog {
fn fetch_from_url(conn: &Connection, url: String) -> Option<Blog> {
let req = Client::new()
.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();
match req {
Ok(mut res) => {
let text = &res.text().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");
let text = &res
.text()
.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
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()))
},
Err(_) => None
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(),
))
}
Err(_) => None,
}
}
@ -137,7 +189,9 @@ impl Blog {
let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance,
None => {
Instance::insert(conn, NewInstance {
Instance::insert(
conn,
NewInstance {
public_domain: inst.clone(),
name: inst.clone(),
local: false,
@ -147,39 +201,91 @@ impl Blog {
default_license: String::new(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new()
})
long_description_html: String::new(),
},
)
}
};
Blog::insert(conn, NewBlog {
actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"),
title: acct.object.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"),
Blog::insert(
conn,
NewBlog {
actor_id: acct
.object
.ap_actor_props
.preferred_username_string()
.expect("Blog::from_activity: preferredUsername error"),
title: acct
.object
.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
})
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 {
let mut blog = Group::default();
blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error");
blog.object_props.set_name_string(self.title.clone()).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");
blog.ap_actor_props
.set_preferred_username_string(self.actor_id.clone())
.expect("Blog::into_activity: preferredUsername error");
blog.object_props
.set_name_string(self.title.clone())
.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();
public_key.set_id_string(format!("{}#main-key", self.ap_url)).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");
public_key
.set_id_string(format!("{}#main-key", self.ap_url))
.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();
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)
}
@ -188,27 +294,41 @@ impl Blog {
let instance = self.get_instance(conn);
if self.outbox_url.len() == 0 {
diesel::update(self)
.set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox")))
.execute(conn).expect("Blog::update_boxes: outbox update error");
.set(blogs::outbox_url.eq(instance.compute_box(
BLOG_PREFIX,
self.actor_id.clone(),
"outbox",
)))
.execute(conn)
.expect("Blog::update_boxes: outbox update error");
}
if self.inbox_url.len() == 0 {
diesel::update(self)
.set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox")))
.execute(conn).expect("Blog::update_boxes: inbox update error");
.set(blogs::inbox_url.eq(instance.compute_box(
BLOG_PREFIX,
self.actor_id.clone(),
"inbox",
)))
.execute(conn)
.expect("Blog::update_boxes: inbox update error");
}
if self.ap_url.len() == 0 {
diesel::update(self)
.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> {
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.set_total_items_u64(self.get_activities(conn).len() as u64).expect("Blog::outbox: count serialization error");
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))
.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)
}
@ -217,35 +337,48 @@ impl Blog {
}
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())
.expect("Blog::get_keypair: pem parsing error"))
.expect("Blog::get_keypair: private key deserialization error")
PKey::from_rsa(
Rsa::private_key_from_pem(
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 {
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()],
links: vec![
Link {
rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None,
href: Some(self.ap_url.clone()),
template: None
template: None,
},
Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")),
href: Some(self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")),
template: None
href: Some(self.get_instance(conn).compute_box(
BLOG_PREFIX,
self.actor_id.clone(),
"feed.atom",
)),
template: None,
},
Link {
rel: String::from("self"),
mime_type: Some(String::from("application/activity+json")),
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(|| {
// The requested blog was not in the DB
// 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)
} else {
None
@ -265,7 +402,11 @@ impl Blog {
if self.instance_id == Instance::local_id(conn) {
self.actor_id.clone()
} else {
format!("{}@{}", self.actor_id, self.get_instance(conn).public_domain)
format!(
"{}@{}",
self.actor_id,
self.get_instance(conn).public_domain
)
}
}
@ -279,7 +420,9 @@ impl Blog {
for post in Post::get_for_blog(conn, &self) {
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> {
let key = self.get_keypair();
let mut signer = Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error");
signer.update(to_sign.as_bytes()).expect("Blog::sign: content insertion error");
signer.sign_to_vec().expect("Blog::sign: finalization error")
let mut signer =
Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization 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 {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).expect("Blog::verify: pem parsing error"))
.expect("Blog::verify: deserialization 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")
let key = PKey::from_rsa(
Rsa::public_key_from_pem(self.public_key.as_ref())
.expect("Blog::verify: pem parsing error"),
).expect("Blog::verify: deserialization 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,
title: String,
summary: String,
instance_id: i32
instance_id: i32,
) -> NewBlog {
let (pub_key, priv_key) = sign::gen_keypair();
NewBlog {
@ -344,7 +499,337 @@ impl NewBlog {
instance_id: instance_id,
ap_url: String::from(""),
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::{
activity::Create,
link,
object::{Note}
};
use activitypub::{activity::Create, link, object::Note};
use chrono::{self, NaiveDateTime};
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
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 mentions::Mention;
use notifications::*;
use plume_common::activity_pub::{
inbox::{FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY,
};
use plume_common::utils;
use posts::Post;
use users::User;
use schema::comments;
use safe_string::SafeString;
use schema::comments;
use users::User;
use Connection;
#[derive(Queryable, Identifiable, Serialize, Clone)]
pub struct Comment {
@ -31,7 +27,7 @@ pub struct Comment {
pub creation_date: NaiveDateTime,
pub ap_url: Option<String>,
pub sensitive: bool,
pub spoiler_text: String
pub spoiler_text: String,
}
#[derive(Insertable, Default)]
@ -43,7 +39,7 @@ pub struct NewComment {
pub author_id: i32,
pub ap_url: Option<String>,
pub sensitive: bool,
pub spoiler_text: String
pub spoiler_text: String,
}
impl Comment {
@ -62,24 +58,35 @@ impl Comment {
pub fn count_local(conn: &Connection) -> usize {
use schema::users;
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id);
comments::table.filter(comments::author_id.eq_any(local_authors))
let local_authors = users::table
.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)
.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 {
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error");
json["author"] = self.get_author(conn).to_json(conn);
let mentions = Mention::list_for_comment(conn, self.id).into_iter()
.map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new()))
let mentions = Mention::list_for_comment(conn, self.id)
.into_iter()
.map(|m| {
m.get_mentioned(conn)
.map(|u| u.get_fqn(conn))
.unwrap_or(String::new())
})
.collect::<Vec<String>>();
json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error");
json["responses"] = json!(others.into_iter()
json["responses"] = json!(
others
.into_iter()
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
.map(|c| c.to_json(conn, others))
.collect::<Vec<_>>());
.collect::<Vec<_>>()
);
json
}
@ -106,61 +113,138 @@ impl Comment {
let mut note = Note::default();
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.set_summary_string(self.spoiler_text.clone()).expect("Comment::into_activity: summary error");
note.object_props.set_content_string(html).expect("Comment::into_activity: content 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| {
let comm = Comment::get(conn, id).expect("Comment::into_activity: comment error");
note.object_props
.set_id_string(self.ap_url.clone().unwrap_or(String::new()))
.expect("Comment::into_activity: id error");
note.object_props
.set_summary_string(self.spoiler_text.clone())
.expect("Comment::into_activity: summary error");
note.object_props
.set_content_string(html)
.expect("Comment::into_activity: content 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| {
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: 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");
note
}
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 mut act = Create::default();
act.create_props.set_actor_link(author.into_id()).expect("Comment::create_activity: actor error");
act.create_props.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.create_props
.set_actor_link(author.into_id())
.expect("Comment::create_activity: actor error");
act.create_props
.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
}
}
impl FromActivity<Note, Connection> for 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 comm = Comment::insert(conn, NewComment {
content: SafeString::new(&note.object_props.content_string().expect("Comment::from_activity: content deserialization error")),
spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")),
let comm = Comment::insert(
conn,
NewComment {
content: SafeString::new(
&note
.object_props
.content_string()
.expect("Comment::from_activity: content deserialization error"),
),
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
});
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
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| {
let author = &Post::get(conn, comm.post_id).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();
let author = &Post::get(conn, comm.post_id)
.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)
}).ok();
})
.ok();
}
}
@ -172,11 +256,14 @@ impl FromActivity<Note, Connection> for Comment {
impl Notify<Connection> for Comment {
fn notify(&self, conn: &Connection) {
for author in self.get_post(conn).get_authors(conn) {
Notification::insert(conn, NewNotification {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::COMMENT.to_string(),
object_id: self.id,
user_id: author.id
});
user_id: author.id,
},
);
}
}
}

View file

@ -1,7 +1,9 @@
use diesel::{
r2d2::{ConnectionManager, Pool, PooledConnection}
use diesel::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 Connection;
@ -23,7 +25,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
let pool = request.guard::<State<DbPool>>()?;
match pool.get() {
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 plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
use {BASE_URL, ap_url, Connection};
use blogs::Blog;
use notifications::*;
use users::User;
use plume_common::activity_pub::{
broadcast,
inbox::{Deletable, FromActivity, Notify, WithInbox},
sign::Signer,
Id, IntoId,
};
use schema::follows;
use users::User;
use {ap_url, Connection, BASE_URL};
#[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(User, foreign_key = "following_id")]
@ -31,22 +40,35 @@ impl Follow {
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))
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 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.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
}
@ -58,23 +80,41 @@ impl Follow {
target: &A,
follow: FollowAct,
from_id: i32,
target_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 {
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();
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");
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");
broadcast(&*target, accept, vec![from.clone()]);
res
}
@ -82,14 +122,41 @@ impl Follow {
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()) {
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");
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)
}
}
@ -98,27 +165,44 @@ impl FromActivity<FollowAct, Connection> for Follow {
impl Notify<Connection> for Follow {
fn notify(&self, conn: &Connection) {
Notification::insert(conn, NewNotification {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::FOLLOW.to_string(),
object_id: self.id,
user_id: self.following_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");
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");
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.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
}

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

View file

@ -1,13 +1,13 @@
use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::iter::Iterator;
use plume_common::utils::md_to_html;
use Connection;
use safe_string::SafeString;
use ap_url;
use users::User;
use plume_common::utils::md_to_html;
use safe_string::SafeString;
use schema::{instances, users};
use users::User;
use Connection;
#[derive(Clone, Identifiable, Queryable, Serialize)]
pub struct Instance {
@ -20,12 +20,12 @@ pub struct Instance {
pub open_registrations: bool,
pub short_description: SafeString,
pub long_description: SafeString,
pub default_license : String,
pub default_license: String,
pub long_description_html: String,
pub short_description_html: String
pub short_description_html: String,
}
#[derive(Insertable)]
#[derive(Clone, Insertable)]
#[table_name = "instances"]
pub struct NewInstance {
pub public_domain: String,
@ -34,28 +34,32 @@ pub struct NewInstance {
pub open_registrations: bool,
pub short_description: SafeString,
pub long_description: SafeString,
pub default_license : String,
pub default_license: String,
pub long_description_html: String,
pub short_description_html: String
pub short_description_html: String,
}
impl 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)
.load::<Instance>(conn)
.expect("Instance::get_local: loading error")
.into_iter().nth(0)
.into_iter()
.nth(0)
}
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)
.expect("Instance::get_remotes: loading error")
}
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())
.limit((max - min).into())
.load::<Instance>(conn)
@ -63,7 +67,9 @@ impl Instance {
}
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);
@ -79,10 +85,12 @@ impl Instance {
/// id: AP object id
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)
.expect("Instance::is_blocked: loading error") {
if id.starts_with(format!("https://{}", block.public_domain).as_str()) {
.expect("Instance::is_blocked: loading error")
{
if id.starts_with(format!("https://{}/", block.public_domain).as_str()) {
return true;
}
}
@ -91,7 +99,8 @@ impl Instance {
}
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))
.load::<User>(conn)
.expect("Instance::has_admin: loading error")
@ -99,14 +108,20 @@ impl Instance {
}
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))
.limit(1)
.get_result::<User>(conn)
.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!(
"{instance}/{prefix}/{name}/{box_name}",
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 (ld, _, _) = md_to_html(long_description.as_ref());
diesel::update(self)
@ -126,12 +148,258 @@ impl Instance {
instances::short_description.eq(short_description),
instances::long_description.eq(long_description),
instances::short_description_html.eq(sd),
instances::long_description_html.eq(ld)
)).execute(conn)
instances::long_description_html.eq(ld),
))
.execute(conn)
.expect("Instance::update: update error");
}
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 webfinger;
#[cfg(test)]
#[macro_use]
extern crate diesel_migrations;
use std::env;
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
@ -99,11 +103,13 @@ macro_rules! list_by {
macro_rules! get {
($table:ident) => {
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)
.load::<Self>(conn)
.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 {
($table:ident) => {
pub fn last(conn: &crate::Connection) -> Self {
$table::table.order_by($table::id.desc())
$table::table
.order_by($table::id.desc())
.limit(1)
.load::<Self>(conn)
.expect(concat!("macro::last: Error getting last ", stringify!($table)))
.iter().next()
.expect(concat!(
"macro::last: Error getting last ",
stringify!($table)
))
.iter()
.next()
.expect(concat!("macro::last: No last ", stringify!($table)))
.clone()
}
@ -189,31 +200,67 @@ macro_rules! last {
}
lazy_static! {
pub static ref BASE_URL: String = env::var("BASE_URL")
.unwrap_or(format!("127.0.0.1:{}", env::var("ROCKET_PORT").unwrap_or(String::from("8000"))));
pub static ref BASE_URL: String = env::var("BASE_URL").unwrap_or(format!(
"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);
}
#[cfg(not(test))]
static DB_NAME: &str = "plume";
#[cfg(test)]
static DB_NAME: &str = "plume_tests";
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
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")))]
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 {
let scheme = if *USE_HTTPS {
"https"
} else {
"http"
};
let scheme = if *USE_HTTPS { "https" } else { "http" };
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 api_tokens;
pub mod apps;

View file

@ -1,18 +1,16 @@
use activitypub::activity;
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 plume_common::activity_pub::{
inbox::{Deletable, FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY,
};
use posts::Post;
use users::User;
use schema::likes;
use users::User;
use Connection;
#[derive(Clone, Queryable, Identifiable)]
pub struct Like {
@ -20,7 +18,7 @@ pub struct Like {
pub user_id: i32,
pub post_id: i32,
pub creation_date: NaiveDateTime,
pub ap_url: String
pub ap_url: String,
}
#[derive(Default, Insertable)]
@ -28,7 +26,7 @@ pub struct Like {
pub struct NewLike {
pub user_id: i32,
pub post_id: i32,
pub ap_url: String
pub ap_url: String,
}
impl Like {
@ -45,17 +43,36 @@ impl Like {
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
)))
.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 {
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.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.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
.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
}
@ -63,13 +80,30 @@ impl Like {
impl FromActivity<activity::Like, Connection> for 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 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 {
let liker = User::from_url(
conn,
like.like_props
.actor
.as_str()
.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(""))
});
ap_url: like.object_props.id_string().unwrap_or(String::from("")),
},
);
res.notify(conn);
res
}
@ -79,30 +113,51 @@ impl Notify<Connection> for Like {
fn notify(&self, conn: &Connection) {
let post = Post::get(conn, self.post_id).expect("Like::notify: post error");
for author in post.get_authors(conn) {
Notification::insert(conn, NewNotification {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::LIKE.to_string(),
object_id: self.id,
user_id: author.id
});
user_id: author.id,
},
);
}
}
}
impl Deletable<Connection, activity::Undo> for Like {
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
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();
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.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.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
.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
}

View file

@ -1,5 +1,5 @@
use activitypub::object::Image;
use diesel::{self, QueryDsl, ExpressionMethods, RunQueryDsl};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID;
use reqwest;
use serde_json;
@ -7,10 +7,10 @@ use std::{fs, path::Path};
use plume_common::activity_pub::Id;
use {ap_url, Connection};
use instance::Instance;
use users::User;
use schema::medias;
use users::User;
use {ap_url, Connection};
#[derive(Clone, Identifiable, Queryable, Serialize)]
pub struct Media {
@ -21,7 +21,7 @@ pub struct Media {
pub remote_url: Option<String>,
pub sensitive: bool,
pub content_warning: Option<String>,
pub owner_id: i32
pub owner_id: i32,
}
#[derive(Insertable)]
@ -33,7 +33,7 @@ pub struct NewMedia {
pub remote_url: Option<String>,
pub sensitive: bool,
pub content_warning: Option<String>,
pub owner_id: i32
pub owner_id: i32,
}
impl Media {
@ -41,29 +41,64 @@ impl Media {
get!(medias);
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 {
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error");
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" => (
"image",
format!("<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!(
"<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),
),
"mp3" | "wav" | "flac" => (
"audio",
format!("<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>", url, self.alt_text),
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text),
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text),
format!(
"<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>",
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" => (
"video",
format!("<video src=\"{}\" title=\"{}\" class=\"preview\"></video>", url, self.alt_text),
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text),
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text),
format!(
"<video src=\"{}\" title=\"{}\" class=\"preview\"></video>",
url, self.alt_text
),
_ => ("unknown", String::new(), String::new(), String::new())
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()),
};
json["html_preview"] = json!(preview);
json["html"] = json!(html);
@ -77,30 +112,43 @@ impl Media {
if self.is_remote {
self.remote_url.clone().unwrap_or(String::new())
} 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) {
if !self.is_remote {
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");
}
diesel::delete(self)
.execute(conn)
.expect("Media::delete: database entry deletion error");
}
pub fn save_remote(conn: &Connection, url: String) -> Media {
Media::insert(conn, NewMedia {
pub fn save_remote(conn: &Connection, url: String, user: &User) -> Media {
Media::insert(
conn,
NewMedia {
file_path: String::new(),
alt_text: String::new(),
is_remote: true,
remote_url: Some(url),
sensitive: false,
content_warning: None,
owner_id: 1 // It will be owned by the admin during an instant, but set_owner will be called just after
})
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)
.set(medias::owner_id.eq(id))
.set(medias::owner_id.eq(user.id))
.execute(conn)
.expect("Media::set_owner: owner update error");
}
@ -108,21 +156,199 @@ impl Media {
// TODO: merge with save_remote?
pub fn from_activity(conn: &Connection, image: Image) -> Option<Media> {
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 path = Path::new("static").join("media").join(format!("{}.{}", GUID::rand().to_string(), ext));
let ext = remote_url
.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()?;
reqwest::get(remote_url.as_str()).ok()?
.copy_to(&mut dest).ok()?;
reqwest::get(remote_url.as_str())
.ok()?
.copy_to(&mut dest)
.ok()?;
Some(Media::insert(conn, NewMedia {
Some(Media::insert(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
is_remote: true,
remote_url: None,
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
}))
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 diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::inbox::Notify;
use Connection;
use comments::Comment;
use notifications::*;
use plume_common::activity_pub::inbox::Notify;
use posts::Post;
use users::User;
use schema::mentions;
use users::User;
use Connection;
#[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)]
pub struct Mention {
@ -15,7 +15,7 @@ pub struct Mention {
pub mentioned_id: i32,
pub post_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)]
@ -24,7 +24,7 @@ pub struct NewMention {
pub mentioned_id: i32,
pub post_id: Option<i32>,
pub comment_id: Option<i32>,
pub ap_url: String
pub ap_url: String,
}
impl Mention {
@ -50,38 +50,62 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Option<User> {
match self.get_post(conn) {
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 {
let user = User::find_by_fqn(conn, ment.clone());
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.link_props.set_name_string(format!("@{}", ment)).expect("Mention::build_activity: name error:");
mention
.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
}
pub fn to_activity(&self, conn: &Connection) -> link::Mention {
let user = self.get_mentioned(conn);
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.link_props.set_name_string(user.map(|u| format!("@{}", u.get_fqn(conn))).unwrap_or(String::new())).expect("Mention::to_activity: mention error");
mention
.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
}
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 mentioned = User::find_by_ap_url(conn, ap_url)?;
if in_post {
Post::get(conn, inside.clone().into()).map(|post| {
let res = Mention::insert(conn, NewMention {
let res = Mention::insert(
conn,
NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
comment_id: None,
ap_url: ment.link_props.href_string().unwrap_or(String::new())
});
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
},
);
if notify {
res.notify(conn);
}
@ -89,12 +113,15 @@ impl Mention {
})
} else {
Comment::get(conn, inside.into()).map(|comment| {
let res = Mention::insert(conn, NewMention {
let res = Mention::insert(
conn,
NewMention {
mentioned_id: mentioned.id,
post_id: None,
comment_id: Some(comment.id),
ap_url: ment.link_props.href_string().unwrap_or(String::new())
});
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
},
);
if notify {
res.notify(conn);
}
@ -106,18 +133,23 @@ impl Mention {
pub fn delete(&self, conn: &Connection) {
//find related notifications and delete them
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 {
fn notify(&self, conn: &Connection) {
self.get_mentioned(conn).map(|m| {
Notification::insert(conn, NewNotification {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::MENTION.to_string(),
object_id: self.id,
user_id: m.id
});
user_id: m.id,
},
);
});
}
}

View file

@ -1,16 +1,16 @@
use chrono::NaiveDateTime;
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use serde_json;
use Connection;
use comments::Comment;
use follows::Follow;
use likes::Like;
use mentions::Mention;
use posts::Post;
use reshares::Reshare;
use users::User;
use schema::notifications;
use users::User;
use Connection;
pub mod notification_kind {
pub const COMMENT: &'static str = "COMMENT";
@ -26,7 +26,7 @@ pub struct Notification {
pub user_id: i32,
pub creation_date: NaiveDateTime,
pub kind: String,
pub object_id: i32
pub object_id: i32,
}
#[derive(Insertable)]
@ -34,7 +34,7 @@ pub struct Notification {
pub struct NewNotification {
pub user_id: i32,
pub kind: String,
pub object_id: i32
pub object_id: i32,
}
impl Notification {
@ -42,14 +42,20 @@ impl Notification {
get!(notifications);
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())
.load::<Notification>(conn)
.expect("Notification::find_for_user: notification loading error")
}
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Vec<Notification> {
notifications::table.filter(notifications::user_id.eq(user.id))
pub fn page_for_user(
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())
.offset(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> {
notifications::table.filter(notifications::kind.eq(kind.into()))
notifications::table
.filter(notifications::kind.eq(kind.into()))
.filter(notifications::object_id.eq(obj))
.get_result::<Notification>(conn)
.ok()
@ -67,25 +74,23 @@ impl Notification {
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
let mut json = json!(self);
json["object"] = json!(match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment|
json!({
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| json!({
"post": comment.get_post(conn).to_json(conn),
"user": comment.get_author(conn).to_json(conn),
"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!({
"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!({
"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))
})
),
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention|
}),
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| {
json!({
"user": mention.get_user(conn).map(|u| u.to_json(conn)),
"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))
})
})
),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare|
}),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| {
json!({
"post": reshare.get_post(conn).map(|p| p.to_json(conn)),
"user": reshare.get_user(conn).map(|u| u.to_json(conn))
})
),
_ => Some(json!({}))
}),
_ => Some(json!({})),
});
json
}
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 users::User;
use schema::post_authors;
use users::User;
#[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(Post)]
@ -10,14 +10,14 @@ use schema::post_authors;
pub struct PostAuthor {
pub id: i32,
pub post_id: i32,
pub author_id: i32
pub author_id: i32,
}
#[derive(Insertable)]
#[table_name = "post_authors"]
pub struct NewPostAuthor {
pub post_id: i32,
pub author_id: i32
pub author_id: i32,
}
impl PostAuthor {

View file

@ -1,36 +1,35 @@
use activitypub::{
activity::{Create, Delete, Update},
link,
object::{Article, Image, Tombstone}
object::{Article, Image, Tombstone},
};
use canapi::{Error, Provider};
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use heck::{CamelCase, KebabCase};
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 instance::Instance;
use likes::Like;
use medias::Media;
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 reshares::Reshare;
use safe_string::SafeString;
use schema::posts;
use std::collections::HashSet;
use tags::Tag;
use users::User;
use schema::posts;
use safe_string::SafeString;
use std::collections::HashSet;
use {ap_url, Connection, BASE_URL};
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")]
@ -68,24 +67,32 @@ pub struct NewPost {
impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
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 !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 {
id: Some(post.id),
title: Some(post.title.clone()),
subtitle: Some(post.subtitle.clone()),
content: Some(post.content.get().clone())
content: Some(post.content.get().clone()),
})
} else {
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();
if let Some(title) = filter.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.get_results::<Post>(*conn).map(|ps| ps.into_iter()
.filter(|p| p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false))
query
.get_results::<Post>(*conn)
.map(|ps| {
ps.into_iter()
.filter(|p| {
p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false)
})
.map(|p| PostEndpoint {
id: Some(p.id),
title: Some(p.title.clone()),
subtitle: Some(p.subtitle.clone()),
content: Some(p.content.get().clone())
content: Some(p.content.get().clone()),
})
.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!()
}
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!()
}
@ -138,7 +158,8 @@ impl Post {
use schema::tags;
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))
.order(posts::creation_date.desc())
.offset(min.into())
@ -150,35 +171,45 @@ impl Post {
pub fn count_for_tag(conn: &Connection, tag: String) -> i64 {
use schema::tags;
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))
.count()
.load(conn)
.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 {
use schema::post_authors;
use schema::users;
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).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))
let local_authors = users::table
.filter(users::instance_id.eq(Instance::local_id(conn)))
.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))
.load::<Post>(conn)
.expect("Post::count_local: loading error")
.len()// TODO count in database?
.len() // TODO count in database?
}
pub fn count(conn: &Connection) -> i64 {
posts::table.filter(posts::published.eq(true))
posts::table
.filter(posts::published.eq(true))
.count()
.get_result(conn)
.expect("Post::count: counting error")
}
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))
.limit(limit)
.load::<Post>(conn)
@ -189,7 +220,8 @@ impl Post {
use schema::post_authors;
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))
.order(posts::creation_date.desc())
.limit(limit)
@ -198,7 +230,8 @@ impl 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))
.order(posts::creation_date.desc())
.limit(limit)
@ -206,15 +239,17 @@ impl Post {
.expect("Post::get_recents_for_blog: loading error")
}
pub fn get_for_blog(conn: &Connection, blog:&Blog) -> Vec<Post> {
posts::table.filter(posts::blog_id.eq(blog.id))
pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Vec<Post> {
posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true))
.load::<Post>(conn)
.expect("Post::get_for_blog:: loading error")
}
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))
.order(posts::creation_date.desc())
.offset(min.into())
@ -225,7 +260,8 @@ impl Post {
/// 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> {
posts::table.order(posts::creation_date.desc())
posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(true))
.offset(min.into())
.limit((max - min).into())
@ -234,12 +270,19 @@ impl Post {
}
/// 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;
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::blog_id.eq_any(blog_ids))
.offset(min.into())
@ -249,13 +292,18 @@ impl Post {
}
/// 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;
let post_ids = post_authors::table
.filter(post_authors::author_id.eq_any(followed))
.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::id.eq_any(post_ids))
.offset(min.into())
@ -268,7 +316,8 @@ impl Post {
use schema::post_authors;
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::id.eq_any(posts))
.load::<Post>(conn)
@ -276,10 +325,13 @@ impl Post {
}
pub fn get_authors(&self, conn: &Connection) -> Vec<User> {
use schema::users;
use schema::post_authors;
use schema::users;
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 {
@ -293,23 +345,28 @@ impl Post {
pub fn get_blog(&self, conn: &Connection) -> Blog {
use schema::blogs;
blogs::table.filter(blogs::id.eq(self.blog_id))
blogs::table
.filter(blogs::id.eq(self.blog_id))
.limit(1)
.load::<Blog>(conn)
.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> {
use schema::likes;
likes::table.filter(likes::post_id.eq(self.id))
likes::table
.filter(likes::post_id.eq(self.id))
.load::<Like>(conn)
.expect("Post::get_likes: loading error")
}
pub fn get_reshares(&self, conn: &Connection) -> Vec<Reshare> {
use schema::reshares;
reshares::table.filter(reshares::post_id.eq(self.id))
reshares::table
.filter(reshares::post_id.eq(self.id))
.load::<Reshare>(conn)
.expect("Post::get_reshares: loading error")
}
@ -318,7 +375,8 @@ impl Post {
if self.ap_url.len() == 0 {
diesel::update(self)
.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")
} else {
self.clone()
@ -326,7 +384,11 @@ impl Post {
}
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| {
for x in f {
acc.push(x.ap_url);
@ -340,74 +402,170 @@ impl Post {
let mut to = self.get_receivers_urls(conn);
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 tags_json = Tag::for_post(conn, self.id).into_iter().map(|t| json!(t.into_activity(conn))).collect::<Vec<serde_json::Value>>();
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 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);
let mut article = Article::default();
article.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");
article
.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
article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Post::into_activity: attributedTo error");
article.object_props.set_content_string(self.content.get().clone()).expect("Post::into_activity: content error");
article.ap_object_props.set_source_object(Source {
article
.object_props
.set_attributed_to_link_vec::<Id>(authors)
.expect("Post::into_activity: attributedTo error");
article
.object_props
.set_content_string(self.content.get().clone())
.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");
})
.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));
if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error");
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 {
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.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");
cover
.object_props
.set_content_string(media.alt_text)
.expect("Post::into_activity: icon.content 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.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
.object_props
.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
}
pub fn create_activity(&self, conn: &Connection) -> Create {
let article = self.into_activity(conn);
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.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::create_activity: Couldn't copy 'to'"))
act.object_props
.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");
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");
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.set_object_object(article).expect("Post::create_activity: object 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
.set_object_object(article)
.expect("Post::create_activity: object error");
act
}
pub fn update_activity(&self, conn: &Connection) -> Update {
let article = self.into_activity(conn);
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.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::update_activity: Couldn't copy 'to'"))
act.object_props
.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");
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");
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.set_object_object(article).expect("Article::update_activity: object 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
.set_object_object(article)
.expect("Article::update_activity: object error");
act
}
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");
if let Ok(title) = updated.object_props.name_string() {
@ -431,7 +589,11 @@ impl Post {
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() {
let mut mentions = vec![];
let mut tags = vec![];
@ -443,13 +605,16 @@ impl Post {
serde_json::from_value::<Hashtag>(tag.clone())
.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) {
hashtags.push(t);
} else {
tags.push(t);
}
}).ok();
})
.ok();
}
post.update_mentions(conn, mentions);
post.update_tags(conn, tags);
@ -460,34 +625,76 @@ impl Post {
}
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
.into_iter()
.map(|m| {
(
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<_>>();
.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_user_mentioned = old_mentions.iter()
.map(|m| m.mentioned_id).collect::<HashSet<_>>();
for (m,id) in mentions.iter() {
let old_user_mentioned = old_mentions
.iter()
.map(|m| m.mentioned_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);
}
}
let new_mentions = mentions.into_iter().map(|(_m,id)| id).collect::<HashSet<_>>();
for m in old_mentions.iter().filter(|m| !new_mentions.contains(&m.mentioned_id)) {
let new_mentions = mentions
.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);
}
}
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_name = old_tags.iter().filter_map(|tag| if !tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)
.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() {
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);
}
}
@ -500,13 +707,31 @@ impl Post {
}
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_name = old_tags.iter().filter_map(|tag| if tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)
.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() {
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);
}
}
@ -532,16 +757,26 @@ impl Post {
}
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 {
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
} 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")
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
@ -550,39 +785,78 @@ impl FromActivity<Article, Connection> for Post {
Some(user) => {
authors.push(user);
(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));
let title = article.object_props.name_string().expect("Post::from_activity: title error");
let post = Post::insert(conn, NewPost {
let title = article
.object_props
.name_string()
.expect("Post::from_activity: title error");
let post = Post::insert(
conn,
NewPost {
blog_id: blog.expect("Post::from_activity: blog not found error").id,
slug: title.to_kebab_case(),
title: title,
content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")),
content: SafeString::new(
&article
.object_props
.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,
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() {
PostAuthor::insert(conn, NewPostAuthor {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id
});
author_id: author.id,
},
);
}
// 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() {
for tag in tags.into_iter() {
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())
.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));
})
.ok();
@ -605,28 +881,44 @@ impl FromActivity<Article, Connection> for Post {
impl Deletable<Connection, Delete> for Post {
fn delete(&self, conn: &Connection) -> Delete {
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();
tombstone.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");
tombstone
.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.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]).expect("Post::delete: to error");
act.object_props
.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) {
m.delete(conn);
}
diesel::delete(self).execute(conn).expect("Post::delete: DB error");
diesel::delete(self)
.execute(conn)
.expect("Post::delete: DB error");
act
}
fn delete_id(id: String, actor_id: String, conn: &Connection) {
let actor = User::find_by_ap_url(conn, actor_id);
let post = Post::find_by_ap_url(conn, id);
let can_delete = actor.and_then(|act|
post.clone().map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
).unwrap_or(false);
let can_delete = actor
.and_then(|act| {
post.clone()
.map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
})
.unwrap_or(false);
if can_delete {
post.map(|p| p.delete(conn));
}

View file

@ -1,13 +1,16 @@
use activitypub::activity::{Announce, Undo};
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 plume_common::activity_pub::{
inbox::{Deletable, FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY,
};
use posts::Post;
use users::User;
use schema::reshares;
use users::User;
use Connection;
#[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)]
pub struct Reshare {
@ -15,7 +18,7 @@ pub struct Reshare {
pub user_id: i32,
pub post_id: i32,
pub ap_url: String,
pub creation_date: NaiveDateTime
pub creation_date: NaiveDateTime,
}
#[derive(Insertable)]
@ -23,29 +26,40 @@ pub struct Reshare {
pub struct NewReshare {
pub user_id: i32,
pub post_id: i32,
pub ap_url: String
pub ap_url: String,
}
impl Reshare {
insert!(reshares, NewReshare);
get!(reshares);
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) {
if self.ap_url.len() == 0 {
diesel::update(self)
.set(reshares::ap_url.eq(format!(
"{}/reshare/{}",
User::get(conn, self.user_id).expect("Reshare::update_ap_url: user error").ap_url,
Post::get(conn, self.post_id).expect("Reshare::update_ap_url: post error").ap_url
User::get(conn, self.user_id)
.expect("Reshare::update_ap_url: user error")
.ap_url,
Post::get(conn, self.post_id)
.expect("Reshare::update_ap_url: post error")
.ap_url
)))
.execute(conn).expect("Reshare::update_ap_url: update error");
.execute(conn)
.expect("Reshare::update_ap_url: update error");
}
}
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())
.limit(limit)
.load::<Reshare>(conn)
@ -62,13 +76,29 @@ impl Reshare {
pub fn into_activity(&self, conn: &Connection) -> Announce {
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");
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");
act.object_props.set_id_string(self.ap_url.clone()).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.object_props
.set_id_string(self.ap_url.clone())
.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
}
@ -76,13 +106,33 @@ impl Reshare {
impl FromActivity<Announce, Connection> for 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 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 {
let user = User::from_url(
conn,
announce
.announce_props
.actor_link::<Id>()
.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(""))
});
ap_url: announce
.object_props
.id_string()
.unwrap_or(String::from("")),
},
);
reshare.notify(conn);
reshare
}
@ -92,30 +142,51 @@ impl Notify<Connection> for Reshare {
fn notify(&self, conn: &Connection) {
let post = self.get_post(conn).expect("Reshare::notify: post error");
for author in post.get_authors(conn) {
Notification::insert(conn, NewNotification {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::RESHARE.to_string(),
object_id: self.id,
user_id: author.id
});
user_id: author.id,
},
);
}
}
}
impl Deletable<Connection, Undo> for Reshare {
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
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();
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.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.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
.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
}

View file

@ -1,13 +1,19 @@
use ammonia::{Builder, UrlRelative};
use serde::{self, Serialize, Deserialize,
Serializer, Deserializer, de::Visitor};
use std::{fmt::{self, Display},
borrow::{Borrow, Cow}, io::Write,
iter, ops::Deref};
use diesel::{self, deserialize::Queryable,
types::ToSql,
use diesel::{
self,
deserialize::Queryable,
serialize::{self, Output},
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! {
static ref CLEAN: Builder<'static> = {
@ -16,17 +22,18 @@ lazy_static! {
.add_tags(iter::once("iframe"))
.id_prefix(Some("postcontent-"))
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
.add_tag_attributes("iframe",
["width", "height", "src", "frameborder"]
.iter()
.map(|&v| v));
.add_tag_attributes(
"iframe",
["width", "height", "src", "frameborder"].iter().map(|&v| v),
);
b
};
}
fn url_add_prefix(url: &str) -> Option<Cow<str>> {
if url.starts_with('#') && ! url.starts_with("#postcontent-") {//if start with an #
let mut new_url = "#postcontent-".to_owned();//change to valid id
if url.starts_with('#') && !url.starts_with("#postcontent-") {
//if start with an #
let mut new_url = "#postcontent-".to_owned(); //change to valid id
new_url.push_str(&url[1..]);
Some(Cow::Owned(new_url))
} 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"]
pub struct SafeString{
pub struct SafeString {
value: String,
}
impl SafeString{
pub fn new(value: &str) -> Self {
SafeString{
impl SafeString {
pub fn new(value: &str) -> Self {
SafeString {
value: CLEAN.clean(&value).to_string(),
}
}
@ -56,7 +63,9 @@ pub fn new(value: &str) -> Self {
impl Serialize for SafeString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer, {
where
S: Serializer,
{
serializer.serialize_str(&self.value)
}
}
@ -66,22 +75,24 @@ struct SafeStringVisitor;
impl<'de> Visitor<'de> for SafeStringVisitor {
type Value = SafeString;
fn expecting(&self, formatter:&mut fmt::Formatter) -> fmt::Result {
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, value: &str) -> Result<SafeString, E>
where E: serde::de::Error{
where
E: serde::de::Error,
{
Ok(SafeString::new(value))
}
}
impl<'de> Deserialize<'de> for SafeString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>, {
Ok(
deserializer.deserialize_string(SafeStringVisitor)?
)
where
D: Deserializer<'de>,
{
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
where
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 {
str::to_sql(&self.value, out)
}
}
impl Borrow<str> for SafeString {
fn borrow(&self) -> &str {
&self.value
@ -137,8 +147,8 @@ impl AsRef<str> for SafeString {
}
}
use rocket::request::FromFormValue;
use rocket::http::RawStr;
use rocket::request::FromFormValue;
impl<'v> FromFormValue<'v> for SafeString {
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 plume_common::activity_pub::Hashtag;
use schema::tags;
use {ap_url, Connection};
#[derive(Clone, Identifiable, Serialize, Queryable)]
pub struct Tag {
pub id: i32,
pub tag: String,
pub is_hashtag: bool,
pub post_id: i32
pub post_id: i32,
}
#[derive(Insertable)]
@ -18,7 +18,7 @@ pub struct Tag {
pub struct NewTag {
pub tag: String,
pub is_hashtag: bool,
pub post_id: i32
pub post_id: i32,
}
impl Tag {
@ -29,33 +29,46 @@ impl Tag {
pub fn into_activity(&self, conn: &Connection) -> Hashtag {
let mut ht = Hashtag::default();
ht.set_href_string(ap_url(format!("{}/tag/{}",
Instance::get_local(conn).expect("Tag::into_activity: local instance not found 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.set_href_string(ap_url(format!(
"{}/tag/{}",
Instance::get_local(conn)
.expect("Tag::into_activity: local instance not found 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
}
pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag {
Tag::insert(conn, NewTag {
Tag::insert(
conn,
NewTag {
tag: tag.name_string().expect("Tag::from_activity: name error"),
is_hashtag,
post_id: post
})
post_id: post,
},
)
}
pub fn build_activity(conn: &Connection, tag: String) -> Hashtag {
let mut ht = Hashtag::default();
ht.set_href_string(ap_url(format!("{}/tag/{}",
Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain,
tag)
)).expect("Tag::into_activity: href error");
ht.set_name_string(tag).expect("Tag::into_activity: name error");
ht.set_href_string(ap_url(format!(
"{}/tag/{}",
Instance::get_local(conn)
.expect("Tag::into_activity: local instance not found error")
.public_domain,
tag
))).expect("Tag::into_activity: href error");
ht.set_name_string(tag)
.expect("Tag::into_activity: name error");
ht
}
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;
#[macro_use] extern crate diesel_migrations;
#[macro_use]
extern crate diesel_migrations;
extern crate plume_models;
use diesel::Connection;
use plume_models::{
DATABASE_URL,
Connection as Conn,
instance::*,
safe_string::SafeString,
};
use plume_models::{Connection as Conn, DATABASE_URL};
#[cfg(feature = "sqlite")]
embed_migrations!("../migrations/sqlite");
@ -24,24 +20,7 @@ fn db() -> Conn {
}
#[test]
fn instance_insert() {
fn empty_test() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
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(())
});
conn.test_transaction::<_, (), _>(|| Ok(()));
}

View file

@ -13,65 +13,102 @@ use activitypub::{
use failure::Error;
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::{
Connection,
comments::Comment,
follows::Follow,
instance::Instance,
likes,
reshares::Reshare,
posts::Post,
users::User
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
users::User, Connection,
};
pub trait Inbox {
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() {
Some(t) => {
match t {
Some(t) => match t {
"Announce" => {
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
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) {
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);
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);
likes::Like::from_activity(
conn,
serde_json::from_value(act.clone())?,
actor_id,
);
Ok(())
}
_ => Err(InboxError::CantUndo)?
"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" => {
@ -79,10 +116,9 @@ pub trait Inbox {
Post::handle_update(conn, act.update_props.object_object()?);
Ok(())
}
_ => Err(InboxError::InvalidType)?
}
_ => Err(InboxError::InvalidType)?,
},
None => Err(InboxError::NoType)?
None => Err(InboxError::NoType)?,
}
}
}

View file

@ -1,36 +1,27 @@
use activitypub::{
activity::Create,
collection::OrderedCollection,
object::Article
};
use activitypub::{activity::Create, collection::OrderedCollection, object::Article};
use atom_syndication::{Entry, FeedBuilder};
use rocket::{
http::{ContentType, Cookies},
request::LenientForm,
response::{Content, Flash, Redirect, status},
http::{ContentType, Cookies}
response::{status, Content, Flash, Redirect},
};
use rocket_contrib::Template;
use serde_json;
use validator::{Validate, ValidationError};
use workerpool::thunk::*;
use inbox::Inbox;
use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId, ApRequest,
inbox::{FromActivity, Notify, Deletable},
sign::{Signable, verify_http_headers}
broadcast,
inbox::{Deletable, FromActivity, Notify},
sign::{verify_http_headers, Signable},
ActivityStream, ApRequest, Id, IntoId,
};
use plume_common::utils;
use plume_models::{
blogs::Blog,
db_conn::DbConn,
follows,
headers::Headers,
instance::Instance,
posts::Post,
reshares::Reshare,
users::*
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::Post,
reshares::Reshare, users::*,
};
use inbox::Inbox;
use routes::Page;
use Worker;
@ -38,13 +29,25 @@ use Worker;
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
match user {
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)]
fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, 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| {
fn details(
name: String,
conn: DbConn,
account: Option<User>,
worker: Worker,
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();
@ -57,10 +60,16 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
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());
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)
Err(e) => {
println!("Error while fetching articles in background: {:?}", e)
}
}
}
}));
@ -69,13 +78,20 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
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 {
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),
});
},
);
}
}));
@ -88,7 +104,9 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
}
}
Template::render("users/details", json!({
Template::render(
"users/details",
json!({
"user": user.to_json(&*conn),
"instance_url": user.get_instance(&*conn).public_domain,
"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>>(),
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
"n_followers": n_followers
}))
})
}),
)
}
)
}
#[get("/dashboard")]
fn dashboard(user: User, conn: DbConn) -> Template {
let blogs = Blog::find_for_author(&*conn, user.id);
Template::render("users/dashboard", json!({
let blogs = Blog::find_for_author(&*conn, &user);
Template::render(
"users/dashboard",
json!({
"account": user.to_json(&*conn),
"blogs": blogs,
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
}))
}),
)
}
#[get("/dashboard", rank = 2)]
fn dashboard_auth() -> Flash<Redirect> {
utils::requires_login(
"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())?;
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
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 {
let f = follows::Follow::insert(&*conn, follows::NewFollow {
let f = follows::Follow::insert(
&*conn,
follows::NewFollow {
follower_id: user.id,
following_id: target.id,
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
});
},
);
f.notify(&*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> {
utils::requires_login(
"You need to be logged in order to follow someone",
uri!(follow: name = name).into()
uri!(follow: name = name).into(),
)
}
#[get("/@/<name>/followers?<page>")]
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!(
account.map(|a| a.to_json(&*conn)),
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),
"instance_url": user.get_instance(&*conn).public_domain,
"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,
"page": page.page,
"n_pages": Page::total(followers_count as i32)
}))
})
}),
)
}
)
}
#[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())
}
#[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)?;
Some(ActivityStream::new(user.into_activity(&*conn)))
}
#[get("/users/new")]
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),
"account": user.map(|u| u.to_json(&*conn)),
"errors": null,
"form": null
}))
}),
)
}
#[get("/@/<name>/edit")]
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
if user.username == name && !name.contains("@") {
Some(Template::render("users/edit", json!({
Some(Template::render(
"users/edit",
json!({
"account": user.to_json(&*conn)
})))
}),
))
} else {
None
}
@ -206,7 +251,7 @@ fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
fn edit_auth(name: String) -> Flash<Redirect> {
utils::requires_login(
"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>")]
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
user.update(&*conn,
data.get().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())
user.update(
&*conn,
data.get()
.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))
}
@ -233,7 +290,9 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
if user.id == account.id {
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)))
} else {
@ -242,16 +301,32 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
}
#[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 {
#[validate(length(min = "1", message = "Username can't be empty"))]
username: String,
#[validate(email(message = "Invalid email"))]
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,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password_confirmation: String
#[validate(
length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
password_confirmation: String,
}
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
@ -264,7 +339,10 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
#[post("/users/new", data = "<data>")]
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
}
@ -278,15 +356,20 @@ fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Temp
false,
String::from(""),
form.email.to_string(),
User::hash_pass(form.password.to_string())
User::hash_pass(form.password.to_string()),
).update_boxes(&*conn);
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),
"errors": e.inner(),
"form": form
})))
}),
)
})
}
#[get("/@/<name>/outbox")]
@ -296,18 +379,32 @@ fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection
}
#[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 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 actor_id = activity["actor"].as_str()
.or_else(|| activity["actor"]["id"].as_str()).ok_or(Some(status::BadRequest(Some("Missing actor id for activity"))))?;
let actor_id = activity["actor"]
.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");
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure() &&
!act.clone().verify(&actor) {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure()
&& !act.clone().verify(&actor)
{
println!(
"Rejected invalid activity supposedly from {}, with headers {:?}",
actor.username, headers.0
);
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")]
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 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();
coll.object_props.set_id_string(user.followers_endpoint).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");
coll.object_props
.set_id_string(user.followers_endpoint)
.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))
}
@ -340,12 +451,19 @@ fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let author = User::find_by_fqn(&*conn, name.clone())?;
let feed = FeedBuilder::default()
.title(author.display_name.clone())
.id(Instance::get_local(&*conn).unwrap().compute_box("~", name, "atom.xml"))
.entries(Post::get_recents_for_author(&*conn, &author, 15)
.id(Instance::get_local(&*conn)
.unwrap()
.compute_box("~", name, "atom.xml"))
.entries(
Post::get_recents_for_author(&*conn, &author, 15)
.into_iter()
.map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>())
.collect::<Vec<Entry>>(),
)
.build()
.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(),
))
}